补充 Auth/Tasks/ScenicPermission/Live 单元测试,并完善 UI Test 登录与会话稳定性。

移除 LoginViewModel 硬编码账号,扩展登录流程测试覆盖;调整 ZLoginSmokeUITests 执行顺序与路由直达逻辑,避免清空 Keychain 影响其它用例。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 12:44:10 +08:00
parent aac8458b04
commit d27438b264
17 changed files with 1104 additions and 36 deletions

View File

@ -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 {