310 lines
12 KiB
Swift
310 lines
12 KiB
Swift
//
|
|
// MaterialManagementViewModelTests.swift
|
|
// suixinkanTests
|
|
//
|
|
|
|
import Foundation
|
|
import XCTest
|
|
@testable import suixinkan
|
|
|
|
/// 素材管理 ViewModel 测试。
|
|
@MainActor
|
|
final class MaterialManagementViewModelTests: XCTestCase {
|
|
func testListLoadsMoreSearchesAndMapsFilterStatus() async {
|
|
let api = MockMaterialAPI()
|
|
api.listResponses = [
|
|
MaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }, order: MaterialOrderInfo(totalNum: 1386, avgOrderAmount: "12", refundTotal: "3")),
|
|
MaterialListResponse(total: 12, list: [makeItem(id: 10), makeItem(id: 11)]),
|
|
MaterialListResponse(total: 1, list: [makeItem(id: 99)]),
|
|
]
|
|
let viewModel = MaterialListViewModel()
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
|
|
await viewModel.selectFilter(.pending, api: api)
|
|
|
|
XCTAssertEqual(api.listCalls.map(\.status), [4, 4, 0])
|
|
XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1])
|
|
XCTAssertEqual(viewModel.items.map(\.id), [99])
|
|
}
|
|
|
|
func testRejectedMaterialCannotToggleListing() async {
|
|
let api = MockMaterialAPI()
|
|
api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 1, status: 0, listingStatus: 0)])]
|
|
let viewModel = MaterialListViewModel()
|
|
var messages: [String] = []
|
|
viewModel.onShowMessage = { messages.append($0) }
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
await viewModel.toggleListing(materialId: 1, checked: true, api: api)
|
|
|
|
XCTAssertEqual(messages.last, "审核通过才可以操作上下架")
|
|
XCTAssertTrue(api.operationCalls.isEmpty)
|
|
}
|
|
|
|
func testApprovedMaterialToggleUpdatesLocalItem() async {
|
|
let api = MockMaterialAPI()
|
|
api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])]
|
|
let viewModel = MaterialListViewModel()
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
await viewModel.toggleListing(materialId: 2, checked: true, api: api)
|
|
|
|
XCTAssertEqual(api.operationCalls.first?.id, 2)
|
|
XCTAssertEqual(api.operationCalls.first?.listingStatus, 1)
|
|
XCTAssertEqual(viewModel.items.first?.listingStatus, 1)
|
|
}
|
|
|
|
func testDetailDeleteCallsCallback() async {
|
|
let api = MockMaterialAPI()
|
|
let viewModel = MaterialDetailViewModel(materialId: 7)
|
|
var deletedId: Int?
|
|
viewModel.onDeleted = { deletedId = $0 }
|
|
|
|
await viewModel.delete(api: api)
|
|
|
|
XCTAssertEqual(api.deletedIds, [7])
|
|
XCTAssertEqual(deletedId, 7)
|
|
}
|
|
|
|
func testUploadValidationMessages() async {
|
|
let api = MockMaterialAPI()
|
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
|
var messages: [String] = []
|
|
viewModel.onShowMessage = { messages.append($0) }
|
|
|
|
await viewModel.submit(api: api)
|
|
viewModel.updateMaterialName("素材")
|
|
await viewModel.submit(api: api)
|
|
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: MockMaterialUploader(urls: ["https://cdn/a.jpg"]))
|
|
await viewModel.submit(api: api)
|
|
await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/c.jpg"]))
|
|
await viewModel.submit(api: api)
|
|
|
|
XCTAssertEqual(messages.prefix(4), [
|
|
"请输入素材名称",
|
|
"请至少上传一个素材",
|
|
"请上传封面图片",
|
|
"请选择关联打卡点",
|
|
])
|
|
XCTAssertNil(api.lastUploadRequest)
|
|
}
|
|
|
|
func testUploadSuccessBuildsMaterialRequestWithTags() async {
|
|
let api = MockMaterialAPI()
|
|
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
|
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg", "https://cdn/c.jpg"])
|
|
var didSucceed = false
|
|
viewModel.onSubmitSuccess = { didSucceed = true }
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
viewModel.updateMaterialName("素材")
|
|
viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
|
|
viewModel.updateTagInput("snow-tag")
|
|
await viewModel.addTag(api: api)
|
|
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
|
|
await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: uploader)
|
|
await viewModel.submit(api: api)
|
|
|
|
XCTAssertTrue(didSucceed)
|
|
XCTAssertEqual(api.lastUploadRequest?.name, "素材")
|
|
XCTAssertEqual(api.lastUploadRequest?.type, "1")
|
|
XCTAssertEqual(api.lastUploadRequest?.mediaType, "1")
|
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/a.jpg")
|
|
XCTAssertEqual(api.lastUploadRequest?.coverUrl, "https://cdn/c.jpg")
|
|
XCTAssertEqual(api.lastUploadRequest?.scenicSpotId, 9)
|
|
XCTAssertEqual(api.lastUploadRequest?.materialTag, "42")
|
|
XCTAssertNil(api.lastUploadRequest?.id)
|
|
}
|
|
|
|
func testCloudMediaImportBuildsMaterialRequestWithoutUploadingAgain() async {
|
|
let api = MockMaterialAPI()
|
|
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
|
var didSucceed = false
|
|
viewModel.onSubmitSuccess = { didSucceed = true }
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
viewModel.updateMaterialName("云盘素材")
|
|
viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
|
|
viewModel.addCloudMediaFiles([
|
|
CloudFile(id: 31, fileUrl: "https://cdn/cloud.jpg", coverUrl: "https://cdn/cover-thumb.jpg", name: "cloud.jpg", type: 2, fileSize: 88),
|
|
])
|
|
await viewModel.setCoverImage(makeUploadItem(fileName: "cover.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/cover.jpg"]))
|
|
await viewModel.submit(api: api)
|
|
|
|
XCTAssertTrue(didSucceed)
|
|
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([31]))
|
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.originalName, "cloud.jpg")
|
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/cloud.jpg")
|
|
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.size, 88)
|
|
}
|
|
|
|
func testCloudMediaImportFiltersByCurrentMaterialTypeAndDeduplicates() {
|
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
|
var messages: [String] = []
|
|
viewModel.onShowMessage = { messages.append($0) }
|
|
|
|
viewModel.addCloudMediaFiles([
|
|
CloudFile(id: 1, fileUrl: "https://cdn/video.mp4", name: "video.mp4", type: 1),
|
|
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
|
|
CloudFile(id: 2, fileUrl: "https://cdn/image.jpg", name: "image.jpg", type: 2),
|
|
])
|
|
|
|
XCTAssertEqual(viewModel.uploadedMediaList.map(\.ossUrl), ["https://cdn/image.jpg"])
|
|
XCTAssertEqual(viewModel.importedCloudFileIDs, Set([2]))
|
|
XCTAssertTrue(messages.isEmpty)
|
|
}
|
|
|
|
func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
|
|
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
|
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg"])
|
|
|
|
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
|
|
await Task.yield()
|
|
|
|
XCTAssertNil(viewModel.uploadDialogState)
|
|
XCTAssertFalse(viewModel.uploadedMediaList.first?.isUploading ?? true)
|
|
XCTAssertEqual(viewModel.uploadedMediaList.first?.uploadProgress, 100)
|
|
}
|
|
|
|
func testEditLoadsDetailAndBuildsEditRequest() async {
|
|
let api = MockMaterialAPI()
|
|
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
|
api.detailResponse = try! decodeDetail("""
|
|
{
|
|
"id": 11,
|
|
"name": "旧素材",
|
|
"status": 1,
|
|
"listing_status": 1,
|
|
"cover_url": "https://cdn/cover.jpg",
|
|
"cover_size": 22,
|
|
"scenic_spot_id": 9,
|
|
"material_tag": [{"id": 3, "name": "cloudy"}],
|
|
"media_list": [{
|
|
"id": 5,
|
|
"original_name": "old.jpg",
|
|
"oss_url": "https://cdn/old.jpg",
|
|
"type": 1,
|
|
"size": 10,
|
|
"width": 100,
|
|
"height": 80
|
|
}]
|
|
}
|
|
""")
|
|
let viewModel = MaterialFormViewModel(mode: .edit(id: 11), currentScenicIdProvider: { 3 })
|
|
|
|
await viewModel.loadInitial(api: api)
|
|
await viewModel.submit(api: api)
|
|
|
|
XCTAssertEqual(viewModel.materialName, "旧素材")
|
|
XCTAssertEqual(viewModel.selectedScenicSpot?.id, 9)
|
|
XCTAssertEqual(viewModel.uploadedMediaList.first?.ossUrl, "https://cdn/old.jpg")
|
|
XCTAssertEqual(api.lastEditRequest?.id, 11)
|
|
XCTAssertEqual(api.lastEditRequest?.type, "1")
|
|
XCTAssertEqual(api.lastEditRequest?.materialTag, "3")
|
|
}
|
|
}
|
|
|
|
/// 素材管理 API mock。
|
|
/// 素材管理 API mock。
|
|
@MainActor
|
|
private final class MockMaterialAPI: MaterialManagementServing {
|
|
var listResponses: [MaterialListResponse] = []
|
|
var listCalls: [(status: Int, keyword: String?, page: Int, pageSize: Int)] = []
|
|
var operationCalls: [(id: Int, listingStatus: Int)] = []
|
|
var deletedIds: [Int] = []
|
|
var tagId = 42
|
|
var lastUploadRequest: MaterialUploadRequest?
|
|
var lastEditRequest: MaterialUploadRequest?
|
|
var spotResponse = ScenicSpotListResponse(list: [], total: 0)
|
|
var detailResponse = try! decodeDetail(#"{"id":1,"status":1,"listing_status":1}"#)
|
|
|
|
func list(status: Int, keyword: String?, page: Int, pageSize: Int) async throws -> MaterialListResponse {
|
|
listCalls.append((status, keyword, page, pageSize))
|
|
guard !listResponses.isEmpty else { return MaterialListResponse() }
|
|
return listResponses.removeFirst()
|
|
}
|
|
|
|
func detail(id: Int) async throws -> MaterialDetail {
|
|
detailResponse
|
|
}
|
|
|
|
func setListingStatus(id: Int, listingStatus: Int) async throws {
|
|
operationCalls.append((id, listingStatus))
|
|
}
|
|
|
|
func delete(id: Int) async throws {
|
|
deletedIds.append(id)
|
|
}
|
|
|
|
func addTag(name: String) async throws -> Int {
|
|
tagId
|
|
}
|
|
|
|
func upload(_ request: MaterialUploadRequest) async throws {
|
|
lastUploadRequest = request
|
|
}
|
|
|
|
func edit(_ request: MaterialUploadRequest) async throws {
|
|
lastEditRequest = request
|
|
}
|
|
|
|
func scenicSpotListAll(scenicId: String) async throws -> ScenicSpotListResponse {
|
|
spotResponse
|
|
}
|
|
}
|
|
|
|
/// 素材管理 OSS 上传 mock。
|
|
/// 素材管理 OSS 上传 mock。
|
|
@MainActor
|
|
private final class MockMaterialUploader: MaterialOSSUploading {
|
|
private var urls: [String]
|
|
|
|
init(urls: [String]) {
|
|
self.urls = urls
|
|
}
|
|
|
|
func uploadMaterialFile(
|
|
data: Data,
|
|
fileName: String,
|
|
fileType: Int,
|
|
scenicId: Int,
|
|
onProgress: @escaping (Int) -> Void
|
|
) async throws -> String {
|
|
onProgress(100)
|
|
return urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst()
|
|
}
|
|
}
|
|
|
|
private func makeItem(id: Int, status: Int = 1, listingStatus: Int = 0) -> MaterialItem {
|
|
MaterialItem(
|
|
id: id,
|
|
name: "素材\(id)",
|
|
coverUrl: "",
|
|
createdAt: "",
|
|
status: status,
|
|
downloadCount: 0,
|
|
likesCount: 0,
|
|
collectCount: 0,
|
|
shareCount: 0,
|
|
listingStatus: listingStatus
|
|
)
|
|
}
|
|
|
|
private func makeUploadItem(fileName: String, ossUrl: String = "") -> MaterialUploadedMediaItem {
|
|
MaterialUploadedMediaItem(
|
|
data: Data(repeating: 1, count: 10),
|
|
fileName: fileName,
|
|
size: 10,
|
|
width: 1,
|
|
height: 1,
|
|
ossUrl: ossUrl
|
|
)
|
|
}
|
|
|
|
private func decodeDetail(_ json: String) throws -> MaterialDetail {
|
|
try JSONDecoder().decode(MaterialDetail.self, from: Data(json.utf8))
|
|
}
|