Files
suixinkan_ios_uikit/suixinkan_iosTests/Projects/ProjectViewModelTests.swift
2026-06-26 14:33:31 +08:00

196 lines
8.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ProjectViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/24.
//
import XCTest
@testable import suixinkan_ios
@MainActor
/// ViewModel
final class ProjectViewModelTests: XCTestCase {
///
func testProjectReloadWithoutScenicClearsListAndSkipsRequest() async {
let api = FakeProjectService()
let viewModel = ProjectManagementViewModel()
await viewModel.reload(api: api, scenicId: nil)
XCTAssertTrue(viewModel.items.isEmpty)
XCTAssertEqual(api.projectListCalls.count, 0)
}
///
func testProjectListReloadAndLoadMore() async {
let api = FakeProjectService()
api.projectPages = [
ListPayload(total: 2, list: [.fixture(id: 1, name: "A")]),
ListPayload(total: 2, list: [.fixture(id: 2, name: "B")])
]
let viewModel = ProjectManagementViewModel()
await viewModel.reload(api: api, scenicId: 9)
await viewModel.loadMore(api: api, scenicId: 9)
XCTAssertEqual(api.projectListCalls.map(\.page), [1, 2])
XCTAssertEqual(viewModel.items.map(\.id).sorted(), [1, 2])
XCTAssertFalse(viewModel.hasMore)
}
///
func testProjectEditorUploadsImagesBeforeCreate() async {
let api = FakeProjectService()
let uploader = FakeProjectUploader()
let viewModel = ProjectEditorViewModel(mode: .create)
viewModel.name = "新项目"
viewModel.descriptionText = "描述"
viewModel.price = "99"
viewModel.otPrice = "199"
viewModel.deposit = "10"
viewModel.selectedSpotIds = [6]
viewModel.coverImage = ProjectLocalImage(data: Data([1]), fileName: "cover.jpg", previewURL: nil)
viewModel.carouselImages = [ProjectLocalImage(data: Data([2]), fileName: "banner.jpg", previewURL: nil)]
let success = await viewModel.submit(scenicId: 9, userId: 100, api: api, uploadService: uploader)
XCTAssertTrue(success)
XCTAssertEqual(uploader.uploadedFileNames, ["cover.jpg", "banner.jpg"])
XCTAssertEqual(api.createdProjects.first?.coverProject, "https://cdn/cover.jpg")
XCTAssertEqual(api.createdProjects.first?.coverCarousel, ["https://cdn/banner.jpg"])
}
///
func testProjectEditorUploadFailureDoesNotCreateProject() async {
let api = FakeProjectService()
let uploader = FakeProjectUploader()
uploader.shouldFail = true
let viewModel = ProjectEditorViewModel(mode: .create)
viewModel.name = "新项目"
viewModel.descriptionText = "描述"
viewModel.price = "99"
viewModel.selectedSpotIds = [6]
viewModel.coverImage = ProjectLocalImage(data: Data([1]), fileName: "cover.jpg", previewURL: nil)
let success = await viewModel.submit(scenicId: 9, userId: 100, api: api, uploadService: uploader)
XCTAssertFalse(success)
XCTAssertTrue(api.createdProjects.isEmpty)
}
/// ID
func testStoreProjectEditorRequiresUserId() async {
let viewModel = StoreProjectEditorViewModel(mode: .create)
let success = await viewModel.submit(userId: 0, api: FakeProjectService(), uploadService: FakeProjectUploader())
XCTAssertFalse(success)
XCTAssertEqual(viewModel.errorMessage, ProjectEditorError.missingUser.localizedDescription)
}
}
private extension PhotographerProjectItem {
///
static func fixture(id: Int, name: String) -> PhotographerProjectItem {
PhotographerProjectItem(id: id, name: name, price: "99.00")
}
}
///
@MainActor
private final class FakeProjectService: ProjectServing {
var projectPages: [ListPayload<PhotographerProjectItem>] = []
var projectListCalls: [(scenicId: Int, name: String?, page: Int, pageSize: Int)] = []
var createdProjects: [ProjectCreateRequest] = []
///
func projectList(scenicId: Int, name: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
projectListCalls.append((scenicId, name, page, pageSize))
return projectPages.isEmpty ? ListPayload(total: 0, list: []) : projectPages.removeFirst()
}
///
func projectDetail(id: Int) async throws -> PhotographerProjectDetailResponse {
try Self.detail()
}
///
func createProject(_ request: ProjectCreateRequest) async throws {
createdProjects.append(request)
}
///
func editProject(_ request: ProjectCreateRequest) async throws {}
///
func deleteProject(id: Int) async throws {}
///
func storeManagerScenicList(userId: String) async throws -> ListPayload<StoreManagerScenicItem> {
ListPayload(total: 0, list: [])
}
///
func storeManagerProjectList(userId: String?, page: Int, pageSize: Int) async throws -> ListPayload<PhotographerProjectItem> {
ListPayload(total: 0, list: [])
}
///
func storeManagerProjectDetail(id: Int) async throws -> PhotographerProjectDetailResponse {
try Self.detail()
}
///
func storeManagerCreate(_ request: StoreManagerCreateRequest) async throws {}
///
func storeManagerOfflineCreate(_ request: StoreManagerOfflineCreateRequest) async throws {}
///
func storeManagerUpdate(_ request: StoreManagerUpdateRequest) async throws {}
///
func storeManagerOfflineUpdate(_ request: StoreManagerOfflineUpdateRequest) async throws {}
///
func storeManagerDeleteProject(id: Int) async throws {}
///
func storeAll() async throws -> ListPayload<StoreItem> {
ListPayload(total: 0, list: [])
}
private static func detail() throws -> PhotographerProjectDetailResponse {
let data = Data(#"{"id":1,"name":"","type":11,"status":1,"price":"99","description":"","material_num":1,"photo_num":1,"video_num":1}"#.utf8)
return try JSONDecoder().decode(PhotographerProjectDetailResponse.self, from: data)
}
}
///
@MainActor
private final class FakeProjectUploader: OSSUploadServing {
var uploadedFileNames: [String] = []
var shouldFail = false
///
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
if shouldFail { throw TestUploadError.failed }
uploadedFileNames.append(fileName)
return "https://cdn/\(fileName)"
}
func uploadUserAvatar(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadRealNameImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadCloudFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadAlbumFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadScenicApplicationImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
func uploadBankCardImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { "" }
}
///
private enum TestUploadError: Error {
case failed
}