// // LiveViewModelTests.swift // suixinkanTests // import Foundation import XCTest @testable import suixinkan @MainActor /// 直播 ViewModel 测试。 final class LiveViewModelTests: XCTestCase { func testLiveManageLoadsMoreCreatesAndControls() async { let api = MockLiveAPI() api.liveListResponses = [ LiveListResponse(total: 12, items: (0 ..< 10).map { liveItem(id: $0) }), LiveListResponse(total: 12, items: [liveItem(id: 10), liveItem(id: 11)]), LiveListResponse(total: 1, items: [liveItem(id: 99)]), LiveListResponse(total: 1, items: [liveItem(id: 99, status: 2)]), LiveListResponse(total: 1, items: [liveItem(id: 99, status: 4)]), ] let viewModel = LiveManageViewModel(currentScenicIdProvider: { 3 }) await viewModel.loadInitial(api: api) await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api) await viewModel.createLive(title: " 新直播 ", coverUrl: "https://cdn/c.jpg", api: api) await viewModel.controlLive(liveItem(id: 99, status: 1), api: api) await viewModel.finishLive(liveItem(id: 99, status: 2), api: api) XCTAssertEqual(api.liveListCalls.map(\.page), [1, 2, 1, 1, 1]) XCTAssertEqual(api.createLiveCalls.first?.title, "新直播") XCTAssertEqual(api.startCalls, [99]) XCTAssertEqual(api.finishCalls, [99]) } func testLiveDetailChangesModeAndStopsLiving() async { let api = MockLiveAPI() api.detailResponses = [ liveItem(id: 8, status: 2, mode: 1), liveItem(id: 8, status: 2, mode: 2), liveItem(id: 8, status: 3, mode: 2), ] let viewModel = LiveDetailViewModel(liveId: 8) await viewModel.load(api: api) await viewModel.changeMode(.smoothFirst, api: api) await viewModel.controlLive(api: api) XCTAssertEqual(viewModel.pushMode, .smoothFirst) XCTAssertEqual(api.modeCalls.first?.mode, 2) XCTAssertEqual(api.stopCalls, [8]) } func testAlbumDateValidationAndDeleteReload() async { let api = MockLiveAPI() api.albumListResponses = [ LiveAlbumFolderListResponse(items: [folder(id: 1)], total: 1), LiveAlbumFolderListResponse(items: [folder(id: 2)], total: 1), LiveAlbumFolderListResponse(items: [], total: 0), ] let viewModel = LiveAlbumListViewModel(currentScenicIdProvider: { 5 }) var messages: [String] = [] viewModel.onShowMessage = { messages.append($0) } let start = date("2026-07-08") let before = date("2026-07-01") await viewModel.loadInitial(api: api) await viewModel.setStartDate(start, api: api) await viewModel.setEndDate(before, api: api) await viewModel.deleteFolder(folder(id: 2), api: api) XCTAssertEqual(messages.last, "结束时间不能小于开始时间") XCTAssertEqual(api.albumListCalls.map(\.startTime), [nil, "2026-07-08", "2026-07-08"]) XCTAssertEqual(api.deleteFolderCalls, [2]) } func testAlbumAddValidationUploadsAndCreates() async { let api = MockLiveAPI() let uploader = MockLiveUploader(urls: ["https://cdn/a.jpg"]) let viewModel = LiveAlbumAddViewModel(currentScenicIdProvider: { 6 }) var messages: [String] = [] viewModel.onShowMessage = { messages.append($0) } await viewModel.create(api: api) viewModel.updateTitle("作品") await viewModel.create(api: api) await viewModel.addLocalMedia(LivePickedMedia(data: Data(repeating: 1, count: 10), fileName: "a.jpg", size: 10, width: 1, height: 1), uploader: uploader) await viewModel.create(api: api) XCTAssertEqual(messages.prefix(2), ["请输入作品名称", "请上传素材"]) XCTAssertEqual(api.createAlbumCalls.first?.name, "作品") XCTAssertEqual(api.createAlbumCalls.first?.items.first?.url, "https://cdn/a.jpg") } func testPreviewDeleteReloadsFolderDetail() async { let api = MockLiveAPI() api.folderDetailResponses = [ folder(id: 4, items: [LiveAlbumFile(url: "a", type: 2, size: 1, id: 10)]), folder(id: 4, items: []), ] let viewModel = LiveAlbumPreviewViewModel(folderId: 4, startIndex: 0) await viewModel.load(api: api) await viewModel.deleteCurrentFile(api: api) XCTAssertEqual(api.deleteFileCalls.first?.folderId, 4) XCTAssertEqual(api.deleteFileCalls.first?.fileId, 10) XCTAssertTrue(viewModel.files.isEmpty) } } @MainActor private final class MockLiveAPI: LiveServing { var liveListResponses: [LiveListResponse] = [] var detailResponses: [LiveItem] = [] var albumListResponses: [LiveAlbumFolderListResponse] = [] var folderDetailResponses: [LiveAlbumFolder] = [] var liveListCalls: [(scenicId: String, page: Int, pageSize: Int)] = [] var createLiveCalls: [(scenicId: String, title: String, coverImg: String)] = [] var startCalls: [Int] = [] var stopCalls: [Int] = [] var finishCalls: [Int] = [] var modeCalls: [(liveId: Int, mode: Int)] = [] var albumListCalls: [(scenicId: String, startTime: String?, endTime: String?, page: Int, pageSize: Int)] = [] var createAlbumCalls: [(scenicId: String, name: String, items: [LiveAlbumFile])] = [] var deleteFolderCalls: [Int] = [] var deleteFileCalls: [(folderId: Int, fileId: Int)] = [] func liveList(scenicId: String, page: Int, pageSize: Int) async throws -> LiveListResponse { liveListCalls.append((scenicId, page, pageSize)) return liveListResponses.isEmpty ? LiveListResponse() : liveListResponses.removeFirst() } func liveDetail(liveId: Int) async throws -> LiveItem { detailResponses.isEmpty ? liveItem(id: liveId) : detailResponses.removeFirst() } func createLive(scenicId: String, title: String, coverImg: String) async throws { createLiveCalls.append((scenicId, title, coverImg)) } func startLive(liveId: Int) async throws { startCalls.append(liveId) } func stopLive(liveId: Int) async throws { stopCalls.append(liveId) } func finishLive(liveId: Int) async throws { finishCalls.append(liveId) } func setPushMode(liveId: Int, mode: Int) async throws { modeCalls.append((liveId, mode)) } func albumList(scenicId: String, startTime: String?, endTime: String?, page: Int, pageSize: Int) async throws -> LiveAlbumFolderListResponse { albumListCalls.append((scenicId, startTime, endTime, page, pageSize)) return albumListResponses.isEmpty ? LiveAlbumFolderListResponse() : albumListResponses.removeFirst() } func createAlbumFolder(scenicId: String, name: String, items: [LiveAlbumFile]) async throws { createAlbumCalls.append((scenicId, name, items)) } func deleteAlbumFolder(folderId: Int) async throws { deleteFolderCalls.append(folderId) } func albumFolderDetail(folderId: Int) async throws -> LiveAlbumFolder { folderDetailResponses.isEmpty ? folder(id: folderId) : folderDetailResponses.removeFirst() } func deleteAlbumFile(folderId: Int, fileId: Int) async throws { deleteFileCalls.append((folderId, fileId)) } } @MainActor private final class MockLiveUploader: LiveOSSUploading { private var urls: [String] init(urls: [String]) { self.urls = urls } func uploadLiveCover(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst() } func uploadLiveAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst() } } private func liveItem(id: Int, status: Int = 1, mode: Int = 1) -> LiveItem { LiveItem(id: id, title: "直播\(id)", coverImg: "", pushUrl: "rtmp://push/\(id)", duration: 60, status: status, statusLabel: "", manualPushMode: mode) } private func folder(id: Int, items: [LiveAlbumFile] = []) -> LiveAlbumFolder { LiveAlbumFolder(id: id, albumId: id, name: "相册\(id)", creator: "", items: items) } private func date(_ text: String) -> Date { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "zh_CN") return formatter.date(from: text)! }