补充 Auth/Tasks/ScenicPermission/Live 单元测试,并完善 UI Test 登录与会话稳定性。
移除 LoginViewModel 硬编码账号,扩展登录流程测试覆盖;调整 ZLoginSmokeUITests 执行顺序与路由直达逻辑,避免清空 Keychain 影响其它用例。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -169,6 +169,186 @@ final class TaskViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(api.cloudFileCalls.map(\.page), [1, 2, 1])
|
||||
}
|
||||
|
||||
/// 测试发布任务会加载可关联订单,无景区时清空列表。
|
||||
func testTaskCreateLoadAvailableOrders() async throws {
|
||||
let api = TaskMockService()
|
||||
api.availableOrders = try TestFixture.payload([AvailableOrderResponse].self, named: "available_order_success")
|
||||
let viewModel = TaskCreateViewModel()
|
||||
|
||||
await viewModel.loadAvailableOrders(api: api, scenicId: nil)
|
||||
XCTAssertTrue(viewModel.availableOrders.isEmpty)
|
||||
XCTAssertTrue(api.availableOrderScenicIds.isEmpty)
|
||||
|
||||
await viewModel.loadAvailableOrders(api: api, scenicId: 88)
|
||||
XCTAssertEqual(api.availableOrderScenicIds, [88])
|
||||
XCTAssertEqual(viewModel.availableOrders.count, 1)
|
||||
XCTAssertFalse(viewModel.isLoadingOrders)
|
||||
}
|
||||
|
||||
/// 测试加载可关联订单失败时会清空列表并提示错误。
|
||||
func testTaskCreateLoadAvailableOrdersFailure() async {
|
||||
let api = TaskMockService()
|
||||
api.availableOrderError = APIError.networkFailed("订单加载失败")
|
||||
let viewModel = TaskCreateViewModel()
|
||||
|
||||
await viewModel.loadAvailableOrders(api: api, scenicId: 88)
|
||||
|
||||
XCTAssertTrue(viewModel.availableOrders.isEmpty)
|
||||
XCTAssertEqual(viewModel.errorMessage, "网络请求失败:订单加载失败")
|
||||
}
|
||||
|
||||
/// 测试上传中的本地附件会阻止提交。
|
||||
func testTaskCreateCanSubmitBlocksWhileUploading() {
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.taskName = "任务"
|
||||
viewModel.remark = "备注"
|
||||
viewModel.selectedLocalFiles = [
|
||||
TaskLocalUploadItem(
|
||||
id: UUID(),
|
||||
data: Data([1]),
|
||||
fileName: "local.jpg",
|
||||
fileType: 2,
|
||||
remark: "",
|
||||
uploadedURL: nil,
|
||||
progress: 50,
|
||||
errorMessage: nil
|
||||
)
|
||||
]
|
||||
|
||||
XCTAssertFalse(viewModel.canSubmit)
|
||||
XCTAssertTrue(viewModel.selectedLocalFiles.first?.isUploading == true)
|
||||
XCTAssertEqual(viewModel.selectedLocalFiles.first?.statusText, "上传中 50%")
|
||||
}
|
||||
|
||||
/// 测试云盘附件合并会去重,移除接口能删除指定文件。
|
||||
func testTaskCreateMergeAndRemoveAttachments() {
|
||||
let viewModel = TaskCreateViewModel()
|
||||
let file = TaskCloudSelectionItem(id: 501, fileName: "cloud.jpg", fileType: 2, remark: "备注")
|
||||
viewModel.mergeCloudFiles([file, file])
|
||||
viewModel.addLocalFile(data: Data([1]), fileName: "photo.jpg")
|
||||
guard let localID = viewModel.selectedLocalFiles.first?.id else {
|
||||
return XCTFail("应存在本地附件")
|
||||
}
|
||||
|
||||
XCTAssertEqual(viewModel.selectedCloudFiles.count, 1)
|
||||
viewModel.removeCloudFile(id: 501)
|
||||
viewModel.removeLocalFile(id: localID)
|
||||
XCTAssertTrue(viewModel.selectedCloudFiles.isEmpty)
|
||||
XCTAssertTrue(viewModel.selectedLocalFiles.isEmpty)
|
||||
}
|
||||
|
||||
/// 测试关联订单和紧急小时会写入发布任务请求。
|
||||
func testTaskCreateSubmitIncludesSelectedOrderAndUrgentHour() async throws {
|
||||
let api = TaskMockService()
|
||||
let upload = TaskMockUploadService()
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.taskName = " 任务 "
|
||||
viewModel.remark = "备注"
|
||||
viewModel.urgentHourText = " 12 "
|
||||
viewModel.selectOrder(taskAvailableOrder(orderNumber: "ORD-1001"))
|
||||
|
||||
let success = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
let request = try XCTUnwrap(api.addTaskRequests.first)
|
||||
XCTAssertEqual(request.name, "任务")
|
||||
XCTAssertEqual(request.orderNumber, "ORD-1001")
|
||||
XCTAssertEqual(request.urgentHour, 12)
|
||||
}
|
||||
|
||||
/// 测试任务名称和紧急小时校验失败时不会提交。
|
||||
func testTaskCreateValidationCoversTaskNameAndUrgentHour() async throws {
|
||||
let api = TaskMockService()
|
||||
let upload = TaskMockUploadService()
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.remark = "备注"
|
||||
|
||||
let missingName = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
XCTAssertFalse(missingName)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请输入任务名称")
|
||||
|
||||
viewModel.taskName = "任务"
|
||||
viewModel.urgentHourText = "abc"
|
||||
let invalidHour = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
XCTAssertFalse(invalidHour)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请输入正确的紧急小时")
|
||||
}
|
||||
|
||||
/// 测试本地视频附件会识别为视频类型。
|
||||
func testTaskCreateDetectsVideoFileType() {
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.addLocalFile(data: Data([1]), fileName: "clip.mp4")
|
||||
|
||||
XCTAssertEqual(viewModel.selectedLocalFiles.first?.fileType, 1)
|
||||
}
|
||||
|
||||
/// 测试云盘选择器支持返回上级目录、搜索筛选和取消选择。
|
||||
func testCloudSelectionPopToFolderSearchAndDeselect() async throws {
|
||||
let api = TaskMockService()
|
||||
let firstPage = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
|
||||
api.cloudFileResponses = [firstPage, firstPage]
|
||||
let viewModel = TaskCloudFileSelectionViewModel()
|
||||
let folder = firstPage.list[0]
|
||||
let file = firstPage.list[1]
|
||||
|
||||
try await viewModel.enterFolder(folder, api: api)
|
||||
viewModel.searchText = " photo "
|
||||
viewModel.selectedFilter = .image
|
||||
try await viewModel.reload(api: api)
|
||||
|
||||
let call = try XCTUnwrap(api.cloudFileCalls.last)
|
||||
XCTAssertEqual(call.parentFolderId, folder.id)
|
||||
XCTAssertEqual(call.name, "photo")
|
||||
XCTAssertEqual(call.type, TaskCloudFileFilter.image.rawValue)
|
||||
|
||||
viewModel.toggleSelection(file)
|
||||
XCTAssertTrue(viewModel.isSelected(file))
|
||||
viewModel.toggleSelection(file)
|
||||
XCTAssertFalse(viewModel.isSelected(file))
|
||||
|
||||
try await viewModel.popToFolder(at: 0, api: api)
|
||||
XCTAssertEqual(viewModel.currentFolderId, 0)
|
||||
}
|
||||
|
||||
/// 测试云盘列表加载失败时会清空状态并抛出错误。
|
||||
func testCloudSelectionReloadFailureClearsState() async {
|
||||
let api = TaskMockService()
|
||||
api.cloudFileError = APIError.networkFailed("云盘失败")
|
||||
let viewModel = TaskCloudFileSelectionViewModel()
|
||||
viewModel.files = [CloudDriveFile(id: 1, name: "占位")]
|
||||
viewModel.total = 5
|
||||
viewModel.page = 2
|
||||
|
||||
do {
|
||||
try await viewModel.reload(api: api)
|
||||
XCTFail("加载失败时应抛出错误")
|
||||
} catch APIError.networkFailed(let message) {
|
||||
XCTAssertEqual(message, "云盘失败")
|
||||
} catch {
|
||||
XCTFail("意外错误: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertTrue(viewModel.files.isEmpty)
|
||||
XCTAssertEqual(viewModel.total, 0)
|
||||
XCTAssertEqual(viewModel.page, 1)
|
||||
XCTAssertEqual(viewModel.errorMessage, "网络请求失败:云盘失败")
|
||||
}
|
||||
|
||||
/// 测试云盘没有更多数据时不会继续分页。
|
||||
func testCloudSelectionLoadMoreStopsWhenNoMore() async throws {
|
||||
let api = TaskMockService()
|
||||
let page = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
|
||||
api.cloudFileResponses = [page]
|
||||
let viewModel = TaskCloudFileSelectionViewModel()
|
||||
|
||||
try await viewModel.reload(api: api)
|
||||
viewModel.total = viewModel.files.count
|
||||
try await viewModel.loadMore(api: api)
|
||||
|
||||
XCTAssertEqual(api.cloudFileCalls.count, 1)
|
||||
XCTAssertFalse(viewModel.isLoadingMore)
|
||||
}
|
||||
|
||||
/// 构造固定日期。
|
||||
private static func date(_ text: String) -> Date {
|
||||
let formatter = DateFormatter()
|
||||
@ -204,7 +384,9 @@ private final class TaskMockService: TaskServing {
|
||||
var taskListResponses: [ListPayload<PhotographerTaskItem>] = []
|
||||
var detailResponse: TaskDetailResponse?
|
||||
var availableOrders: [AvailableOrderResponse] = []
|
||||
var availableOrderError: Error?
|
||||
var cloudFileResponses: [ListPayload<CloudDriveFile>] = []
|
||||
var cloudFileError: Error?
|
||||
private(set) var taskListCalls: [TaskListCall] = []
|
||||
private(set) var detailIds: [Int] = []
|
||||
private(set) var availableOrderScenicIds: [Int] = []
|
||||
@ -244,6 +426,7 @@ private final class TaskMockService: TaskServing {
|
||||
/// 记录可关联订单请求并返回响应。
|
||||
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
|
||||
availableOrderScenicIds.append(scenicId)
|
||||
if let availableOrderError { throw availableOrderError }
|
||||
return availableOrders
|
||||
}
|
||||
|
||||
@ -271,10 +454,17 @@ private final class TaskMockService: TaskServing {
|
||||
pageSize: pageSize
|
||||
)
|
||||
)
|
||||
if let cloudFileError { throw cloudFileError }
|
||||
return cloudFileResponses.isEmpty ? ListPayload(total: 0, list: []) : cloudFileResponses.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造可关联订单测试数据。
|
||||
private func taskAvailableOrder(orderNumber: String) -> AvailableOrderResponse {
|
||||
let data = Data(#"{"project_name":"项目","order_number":"\#(orderNumber)","order_status":"1","order_status_label":"已支付","pay_time":"2026-06-24","user_phone":"13800000000"}"#.utf8)
|
||||
return try! JSONDecoder().decode(AvailableOrderResponse.self, from: data)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// OSS 上传服务测试替身。
|
||||
private final class TaskMockUploadService: OSSUploadServing {
|
||||
|
||||
Reference in New Issue
Block a user