Add Projects, Schedule, and Invite modules with home routing.
Migrate project management, schedule management, and photographer invitation flows from placeholders, including project image OSS upload and shared business models. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
195
suixinkanTests/Projects/ProjectViewModelTests.swift
Normal file
195
suixinkanTests/Projects/ProjectViewModelTests.swift
Normal file
@ -0,0 +1,195 @@
|
||||
//
|
||||
// ProjectViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@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
|
||||
}
|
||||
Reference in New Issue
Block a user