// // CloudDriveViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan @MainActor /// 相册云盘 ViewModel 测试。 final class CloudDriveViewModelTests: XCTestCase { func testDisplayModeToggleSwitchesBetweenGridAndListAndNotifiesUI() { let viewModel = CloudDriveListViewModel() var notificationCount = 0 viewModel.onStateChange = { notificationCount += 1 } viewModel.toggleDisplayMode() XCTAssertEqual(viewModel.displayMode, .list) viewModel.toggleDisplayMode() XCTAssertEqual(viewModel.displayMode, .grid) XCTAssertEqual(notificationCount, 2) } func testListFileDescriptionMatchesAndroidDateSizeAndFolderCount() { let image = CloudFile( id: 1, name: "海边.jpg", createdAt: "2026-07-10 18:32:15", type: 2, fileSize: 2_048 ) let folder = CloudFile( id: 2, name: "旅行", createdAt: "2026-07-09 09:08:07", childNum: 3, type: 99 ) XCTAssertEqual(CloudDriveFormatter.fileDescription(image), "2026-07-10 | 2.00 KB") XCTAssertEqual(CloudDriveFormatter.fileDescription(folder), "2026-07-09 | 3个项目") XCTAssertEqual(CloudDriveFormatter.fileSize(0), "0 B") } func testLoadMoreSearchFilterSortAndFolderNavigation() async throws { let api = MockCloudDriveAPI() api.listResponses = [ CloudFileListResponse(total: 3, list: [file(id: 1), folder(id: 2)]), CloudFileListResponse(total: 3, list: [file(id: 3)]), CloudFileListResponse(total: 1, list: [file(id: 4)]), CloudFileListResponse(total: 1, list: [file(id: 5)]), CloudFileListResponse(total: 0, list: []), ] let viewModel = CloudDriveListViewModel() await viewModel.refresh(api: api) await viewModel.loadMoreIfNeeded(lastVisibleIndex: 1, api: api) viewModel.updateSearchText("图") await viewModel.chooseFilterType(.image, api: api) await viewModel.chooseSortType(.createdAscending, api: api) _ = await viewModel.handleFileTap(folder(id: 9), api: api) XCTAssertEqual(viewModel.files.map(\.id), []) XCTAssertEqual(viewModel.pathStack.map(\.id), [0, 9]) XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1, 1, 1]) XCTAssertEqual(api.listCalls[2].name, "图") XCTAssertEqual(api.listCalls[2].type, 2) XCTAssertEqual(api.listCalls[3].orderBy, 1) XCTAssertEqual(api.listCalls[4].name, "") } func testBackRestoresCachedRootAndGoHome() async { let api = MockCloudDriveAPI() api.listResponses = [ CloudFileListResponse(total: 1, list: [folder(id: 2)]), CloudFileListResponse(total: 1, list: [file(id: 5)]), ] let viewModel = CloudDriveListViewModel() await viewModel.refresh(api: api) _ = await viewModel.handleFileTap(folder(id: 2), api: api) let handled = await viewModel.navigateBack(api: api) XCTAssertTrue(handled) XCTAssertEqual(viewModel.pathStack.map(\.id), [0]) XCTAssertEqual(viewModel.files.map(\.id), [2]) XCTAssertEqual(api.listCalls.count, 2) } func testRootBackReturnsFalseWithoutRequest() async { let api = MockCloudDriveAPI() let viewModel = CloudDriveListViewModel() let handled = await viewModel.navigateBack(api: api) XCTAssertFalse(handled) XCTAssertEqual(viewModel.pathStack.map(\.id), [0]) XCTAssertTrue(api.listCalls.isEmpty) } func testBreadcrumbAndGoHomeRestoreFolderCaches() async { let api = MockCloudDriveAPI() api.listResponses = [ CloudFileListResponse(total: 1, list: [folder(id: 2)]), CloudFileListResponse(total: 1, list: [folder(id: 3)]), CloudFileListResponse(total: 1, list: [file(id: 9)]), ] let viewModel = CloudDriveListViewModel() await viewModel.refresh(api: api) _ = await viewModel.handleFileTap(folder(id: 2), api: api) _ = await viewModel.handleFileTap(folder(id: 3), api: api) await viewModel.navigateToPathIndex(1, api: api) XCTAssertEqual(viewModel.pathStack.map(\.id), [0, 2]) XCTAssertEqual(viewModel.files.map(\.id), [3]) XCTAssertEqual(api.listCalls.count, 3) await viewModel.goHome(api: api) XCTAssertEqual(viewModel.pathStack.map(\.id), [0]) XCTAssertEqual(viewModel.files.map(\.id), [2]) XCTAssertEqual(api.listCalls.count, 3) } func testFolderDepthLimitPreventsSixthLevel() async { let api = MockCloudDriveAPI() let viewModel = CloudDriveListViewModel() var message: String? viewModel.onShowMessage = { message = $0 } for id in 1 ... 4 { _ = await viewModel.handleFileTap(folder(id: id), api: api) } await viewModel.addFolder(name: "第六层", api: api) XCTAssertEqual(viewModel.pathStack.count, 5) XCTAssertEqual(message, "不能创建更深的文件夹") XCTAssertTrue(api.createFolderCalls.isEmpty) } func testCreateDeleteMoveAndRenameRefresh() async { let api = MockCloudDriveAPI() api.listResponses = Array(repeating: CloudFileListResponse(), count: 5) let viewModel = CloudDriveListViewModel() await viewModel.addFolder(name: " 新目录 ", api: api) viewModel.setControlFile(file(id: 7, type: 2)) await viewModel.deleteCurrentFile(api: api) await viewModel.moveCurrentFile(targetFolderId: 9, api: api) await viewModel.saveCurrentFileName(" 新名.jpg ", api: api) viewModel.setControlFile(folder(id: 8)) await viewModel.saveCurrentFileName(" 新目录名 ", api: api) XCTAssertEqual(api.createFolderCalls.first?.name, "新目录") XCTAssertEqual(api.deleteCalls.first?.list.first?.id, 7) XCTAssertEqual(api.moveCalls.first?.targetFolderId, 9) XCTAssertEqual(api.renameFileCalls.first?.fileName, "新名.jpg") XCTAssertEqual(api.renameFolderCalls.first?.name, "新目录名") } func testUploadPermissionFailureShowsReason() async { let api = MockCloudDriveAPI() api.permission = CloudUploadPermissionResponse(canUpload: false, reason: "空间不足") let viewModel = CloudDriveListViewModel() var message: String? viewModel.onShowMessage = { message = $0 } let canUpload = await viewModel.checkCanUpload(api: api) XCTAssertFalse(canUpload) XCTAssertEqual(message, "空间不足") } func testEmptyFolderAndRenameNamesAreRejected() async { let api = MockCloudDriveAPI() let viewModel = CloudDriveListViewModel() var messages: [String] = [] viewModel.onShowMessage = { messages.append($0) } await viewModel.addFolder(name: " ", api: api) viewModel.setControlFile(file(id: 7)) await viewModel.saveCurrentFileName("\n", api: api) XCTAssertEqual(messages, ["名称不能为空", "名称不能为空"]) XCTAssertTrue(api.createFolderCalls.isEmpty) XCTAssertTrue(api.renameFileCalls.isEmpty) } func testUploadCompletionRefreshesOnlyCurrentFolder() async { let api = MockCloudDriveAPI() api.listResponses = [ CloudFileListResponse(total: 1, list: [file(id: 1)]), CloudFileListResponse(total: 1, list: [file(id: 2)]), ] let viewModel = CloudDriveListViewModel() await viewModel.refresh(api: api) await viewModel.handleUploadCompleted(parentFolderId: 9, api: api) XCTAssertEqual(api.listCalls.count, 1) await viewModel.handleUploadCompleted(parentFolderId: 0, api: api) XCTAssertEqual(api.listCalls.count, 2) XCTAssertEqual(viewModel.files.map(\.id), [2]) } private func file(id: Int, type: Int = 2) -> CloudFile { CloudFile(id: id, fileUrl: "https://cdn/\(id).jpg", name: "文件\(id)", type: type) } private func folder(id: Int) -> CloudFile { CloudFile(id: id, name: "目录\(id)", type: 99) } } @MainActor private final class MockCloudDriveAPI: CloudDriveServing { var listResponses: [CloudFileListResponse] = [] var permission = CloudUploadPermissionResponse(canUpload: true, reason: "") var listCalls: [(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int)] = [] var createFolderCalls: [(parentFolderId: Int, name: String)] = [] var uploadCalls: [(parentFolderId: Int, fileUrl: String, fileName: String)] = [] var deleteCalls: [CloudFileDeleteRequest] = [] var moveCalls: [CloudFileMoveRequest] = [] var renameFolderCalls: [(fileId: Int, name: String)] = [] var renameFileCalls: [(fileId: Int, fileName: String)] = [] func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> CloudFileListResponse { listCalls.append((parentFolderId, name, type, orderBy, page, pageSize)) return listResponses.isEmpty ? CloudFileListResponse() : listResponses.removeFirst() } func createCloudFolder(parentFolderId: Int, name: String) async throws { createFolderCalls.append((parentFolderId, name)) } func uploadCloudFile(parentFolderId: Int, fileUrl: String, fileName: String) async throws { uploadCalls.append((parentFolderId, fileUrl, fileName)) } func deleteCloudFiles(_ request: CloudFileDeleteRequest) async throws { deleteCalls.append(request) } func moveCloudFiles(_ request: CloudFileMoveRequest) async throws { moveCalls.append(request) } func checkUploadPermission() async throws -> CloudUploadPermissionResponse { permission } func modifyCloudFolderName(fileId: Int, name: String) async throws { renameFolderCalls.append((fileId, name)) } func modifyCloudFileName(fileId: Int, fileName: String) async throws { renameFileCalls.append((fileId, fileName)) } }