新增任务模块,并接入首页任务管理路由
实现任务列表、详情与创建页面,对接 API,支持云端/本地附件,并添加单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
suixinkanTests/Fixtures/available_order_success.json
Normal file
14
suixinkanTests/Fixtures/available_order_success.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"project_name": "亲子跟拍",
|
||||
"order_number": "ORD-TASK-001",
|
||||
"order_status": "20",
|
||||
"order_status_label": "已支付",
|
||||
"pay_time": "2026-06-21 09:55:00",
|
||||
"user_phone": "13800000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
suixinkanTests/Fixtures/cloud_file_list_success.json
Normal file
33
suixinkanTests/Fixtures/cloud_file_list_success.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"total": "2",
|
||||
"list": [
|
||||
{
|
||||
"id": "501",
|
||||
"parent_folder_id": "0",
|
||||
"file_url": "",
|
||||
"cover_url": "",
|
||||
"updated_at": "2026-06-20 10:00:00",
|
||||
"name": "任务素材",
|
||||
"created_at": "2026-06-20 09:00:00",
|
||||
"child_num": "3",
|
||||
"type": "99",
|
||||
"file_size": "0"
|
||||
},
|
||||
{
|
||||
"id": 502,
|
||||
"parent_folder_id": 0,
|
||||
"file_url": "https://cdn.example.com/cloud/photo.jpg",
|
||||
"cover_url": "",
|
||||
"updated_at": "2026-06-20 10:00:00",
|
||||
"name": "photo.jpg",
|
||||
"created_at": "2026-06-20 09:00:00",
|
||||
"child_num": 0,
|
||||
"type": 2,
|
||||
"file_size": 2048
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
38
suixinkanTests/Fixtures/task_detail_success.json
Normal file
38
suixinkanTests/Fixtures/task_detail_success.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": "101",
|
||||
"name": "游客精修任务",
|
||||
"created_at": "2026-06-21 10:00:00",
|
||||
"task_status": "2",
|
||||
"task_status_label": "处理中",
|
||||
"accepted_at": "2026-06-21 10:05:00",
|
||||
"editor_name": "小李",
|
||||
"editor_phone": "13900000000",
|
||||
"urgent_hour": "24",
|
||||
"photog_remark": "请优先处理亲子照片",
|
||||
"order": {
|
||||
"project_name": "亲子跟拍",
|
||||
"order_number": "ORD-TASK-001",
|
||||
"pay_time": "2026-06-21 09:55:00",
|
||||
"user_phone": "13800000000"
|
||||
},
|
||||
"task_result": [
|
||||
{
|
||||
"id": 9101,
|
||||
"photog_task_id": 101,
|
||||
"user_id": 7001,
|
||||
"file_name": "result.mp4",
|
||||
"file_type": 1,
|
||||
"file_url": "https://cdn.example.com/task/result.mp4",
|
||||
"file_size": 4096,
|
||||
"cover_url": "https://cdn.example.com/task/result-cover.jpg",
|
||||
"duration": 12,
|
||||
"remark": "",
|
||||
"created_at": "2026-06-21 11:00:00",
|
||||
"updated_at": "2026-06-21 11:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
26
suixinkanTests/Fixtures/task_list_page2_success.json
Normal file
26
suixinkanTests/Fixtures/task_list_page2_success.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"total": 3,
|
||||
"list": [
|
||||
{
|
||||
"id": 103,
|
||||
"name": "补传照片",
|
||||
"type": 1,
|
||||
"order_number": "ORD-TASK-003",
|
||||
"task_status": 3,
|
||||
"status_name": "已完成",
|
||||
"created_at": "2026-06-23 09:00:00",
|
||||
"updated_at": "2026-06-23 09:30:00",
|
||||
"photog_remark": "",
|
||||
"editor_name": "小张",
|
||||
"operate_time": "",
|
||||
"order_user_nickname": "",
|
||||
"order_user_phone": "",
|
||||
"order_user_id": "",
|
||||
"media": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
58
suixinkanTests/Fixtures/task_list_success.json
Normal file
58
suixinkanTests/Fixtures/task_list_success.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"total": "2",
|
||||
"list": [
|
||||
{
|
||||
"id": "101",
|
||||
"name": "游客精修任务",
|
||||
"type": "1",
|
||||
"order_number": "ORD-TASK-001",
|
||||
"task_status": "2",
|
||||
"status_name": "处理中",
|
||||
"created_at": "2026-06-21 10:00:00",
|
||||
"updated_at": "2026-06-21 10:30:00",
|
||||
"photog_remark": "请优先处理亲子照片",
|
||||
"editor_name": "小李",
|
||||
"operate_time": "2026-06-21 10:31:00",
|
||||
"order_user_nickname": "王女士",
|
||||
"order_user_phone": "13800000000",
|
||||
"order_user_id": "7001",
|
||||
"media": [
|
||||
{
|
||||
"id": "9001",
|
||||
"photog_task_id": "101",
|
||||
"user_id": "7001",
|
||||
"file_name": "cover.jpg",
|
||||
"file_type": "2",
|
||||
"file_url": "https://cdn.example.com/task/cover.jpg",
|
||||
"file_size": "2048",
|
||||
"cover_url": "",
|
||||
"duration": "0",
|
||||
"remark": "",
|
||||
"created_at": "2026-06-21 10:01:00",
|
||||
"updated_at": "2026-06-21 10:01:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "短视频剪辑",
|
||||
"type": 1,
|
||||
"order_number": "ORD-TASK-002",
|
||||
"task_status": 1,
|
||||
"status_name": "待接收",
|
||||
"created_at": "2026-06-22 09:00:00",
|
||||
"updated_at": "2026-06-22 09:00:00",
|
||||
"photog_remark": "",
|
||||
"editor_name": "",
|
||||
"operate_time": "",
|
||||
"order_user_nickname": "",
|
||||
"order_user_phone": "",
|
||||
"order_user_id": "",
|
||||
"media": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -32,14 +32,13 @@ final class HomeMenuRouterTests: XCTestCase {
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "permission_apply", title: ""), .destination(.permissionApply))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "permission_apply_status", title: ""), .destination(.permissionApplyStatus))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenicapplication", title: ""), .destination(.scenicApplication))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_management", title: ""), .destination(.taskManagement))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_management_editor", title: ""), .destination(.taskManagement))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_create", title: ""), .destination(.taskCreate))
|
||||
}
|
||||
|
||||
/// 测试已知但未迁移的首页路由会进入安全占位。
|
||||
func testKnownUnmigratedRoutesResolveToPlaceholders() {
|
||||
XCTAssertEqual(
|
||||
HomeMenuRouter.resolve(uri: "task_management", title: ""),
|
||||
.destination(.modulePlaceholder(uri: "task_management", title: "任务管理"))
|
||||
)
|
||||
XCTAssertEqual(
|
||||
HomeMenuRouter.resolve(uri: "scenic_settlement", title: ""),
|
||||
.destination(.modulePlaceholder(uri: "scenic_settlement", title: "景区结算"))
|
||||
|
||||
145
suixinkanTests/TaskAPITests.swift
Normal file
145
suixinkanTests/TaskAPITests.swift
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// TaskAPITests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 任务 API 测试,覆盖请求参数、请求体和模型解码。
|
||||
final class TaskAPITests: XCTestCase {
|
||||
/// 测试任务列表接口使用正确 path 和 query。
|
||||
func testTaskListUsesExpectedPathAndQuery() async throws {
|
||||
let session = TaskRecordingURLSession(data: try TestFixture.data(named: "task_list_success"))
|
||||
let api = TaskAPI(client: APIClient(session: session))
|
||||
|
||||
_ = try await api.taskList(
|
||||
scenicId: 88,
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
taskStatus: -2,
|
||||
taskName: " 精修 ",
|
||||
startTime: "2026-06-01",
|
||||
endTime: "2026-06-30"
|
||||
)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/task/list")
|
||||
let query = taskQueryItems(from: request)
|
||||
XCTAssertEqual(query["scenic_id"], "88")
|
||||
XCTAssertEqual(query["page"], "1")
|
||||
XCTAssertEqual(query["page_size"], "1")
|
||||
XCTAssertEqual(query["task_status"], "0")
|
||||
XCTAssertEqual(query["task_name"], "精修")
|
||||
XCTAssertEqual(query["start_time"], "2026-06-01")
|
||||
XCTAssertEqual(query["end_time"], "2026-06-30")
|
||||
}
|
||||
|
||||
/// 测试任务详情接口使用正确 path 和 id。
|
||||
func testTaskDetailUsesExpectedPath() async throws {
|
||||
let session = TaskRecordingURLSession(data: try TestFixture.data(named: "task_detail_success"))
|
||||
let api = TaskAPI(client: APIClient(session: session))
|
||||
|
||||
let detail = try await api.taskDetail(id: 101)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/task/info")
|
||||
XCTAssertEqual(taskQueryItems(from: request)["id"], "101")
|
||||
XCTAssertEqual(detail.urgentHour, 24)
|
||||
XCTAssertEqual(detail.taskResult.first?.isVideo, true)
|
||||
}
|
||||
|
||||
/// 测试可关联订单接口使用正确景区参数。
|
||||
func testAvailableOrderUsesScenicId() async throws {
|
||||
let session = TaskRecordingURLSession(data: try TestFixture.data(named: "available_order_success"))
|
||||
let api = TaskAPI(client: APIClient(session: session))
|
||||
|
||||
let orders = try await api.availableOrderList(scenicId: 88)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/task/available-order")
|
||||
XCTAssertEqual(taskQueryItems(from: request)["scenic_id"], "88")
|
||||
XCTAssertEqual(orders.first?.orderNumber, "ORD-TASK-001")
|
||||
}
|
||||
|
||||
/// 测试发布任务接口提交正确 body。
|
||||
func testAddTaskUsesExpectedBody() async throws {
|
||||
let session = TaskRecordingURLSession(data: try TestFixture.data(named: "empty_success"))
|
||||
let api = TaskAPI(client: APIClient(session: session))
|
||||
let request = AddTaskRequest(
|
||||
scenicId: "88",
|
||||
name: "任务",
|
||||
orderNumber: "ORD001",
|
||||
remark: "备注",
|
||||
urgentHour: 12,
|
||||
cloudFile: [CloudFileItem(fileId: 1, remark: "")],
|
||||
uploadFile: [UploadFileItem(fileUrl: "https://cdn.example.com/a.jpg", fileName: "a.jpg", remark: "")]
|
||||
)
|
||||
|
||||
try await api.addTask(request)
|
||||
|
||||
let urlRequest = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(urlRequest.url?.path, "/api/yf-handset-app/photog/task/create")
|
||||
let body = try XCTUnwrap(urlRequest.httpBody)
|
||||
let object = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
|
||||
XCTAssertEqual(object["scenic_id"] as? String, "88")
|
||||
XCTAssertEqual(object["name"] as? String, "任务")
|
||||
XCTAssertEqual(object["order_number"] as? String, "ORD001")
|
||||
XCTAssertEqual(object["photog_remark"] as? String, "备注")
|
||||
XCTAssertEqual(object["urgent_hour"] as? Int, 12)
|
||||
XCTAssertEqual((object["cloud_file"] as? [[String: Any]])?.first?["file_id"] as? Int, 1)
|
||||
XCTAssertEqual((object["upload_file"] as? [[String: Any]])?.first?["file_url"] as? String, "https://cdn.example.com/a.jpg")
|
||||
}
|
||||
|
||||
/// 测试云盘文件列表接口使用正确 path 和 query。
|
||||
func testCloudFileListUsesExpectedQuery() async throws {
|
||||
let session = TaskRecordingURLSession(data: try TestFixture.data(named: "cloud_file_list_success"))
|
||||
let api = TaskAPI(client: APIClient(session: session))
|
||||
|
||||
let payload = try await api.cloudFileList(parentFolderId: 501, name: "photo", type: 2, orderBy: 2, page: 0, pageSize: 0)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/cloud-driver/list")
|
||||
let query = taskQueryItems(from: request)
|
||||
XCTAssertEqual(query["parent_folder_id"], "501")
|
||||
XCTAssertEqual(query["name"], "photo")
|
||||
XCTAssertEqual(query["type"], "2")
|
||||
XCTAssertEqual(query["page"], "1")
|
||||
XCTAssertEqual(query["page_size"], "1")
|
||||
XCTAssertEqual(payload.list.first?.isFolder, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务 API 测试用 URLSession,记录请求并返回固定数据。
|
||||
private final class TaskRecordingURLSession: URLSessionProtocol {
|
||||
let data: Data
|
||||
private(set) var requests: [URLRequest] = []
|
||||
|
||||
/// 初始化测试 Session。
|
||||
init(data: Data) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
/// 记录请求并返回成功响应。
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
requests.append(request)
|
||||
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
return (data, response)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从请求中提取 query 字典。
|
||||
private func taskQueryItems(from request: URLRequest) -> [String: String] {
|
||||
guard
|
||||
let url = request.url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
return [:]
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
|
||||
item.value.map { (item.name, $0) }
|
||||
})
|
||||
}
|
||||
323
suixinkanTests/TaskViewModelTests.swift
Normal file
323
suixinkanTests/TaskViewModelTests.swift
Normal file
@ -0,0 +1,323 @@
|
||||
//
|
||||
// TaskViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 任务 ViewModel 测试,覆盖列表、发布任务和云盘选择逻辑。
|
||||
final class TaskViewModelTests: XCTestCase {
|
||||
/// 测试无景区时清空任务列表且不请求接口。
|
||||
func testTaskManagementClearsWithoutScenic() async throws {
|
||||
let api = TaskMockService()
|
||||
api.taskListResponses = [try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success")]
|
||||
let viewModel = TaskManagementViewModel()
|
||||
|
||||
try await viewModel.reload(api: api, scenicId: 88)
|
||||
XCTAssertFalse(viewModel.tasks.isEmpty)
|
||||
|
||||
try await viewModel.reload(api: api, scenicId: nil)
|
||||
|
||||
XCTAssertTrue(viewModel.tasks.isEmpty)
|
||||
XCTAssertEqual(viewModel.total, 0)
|
||||
XCTAssertEqual(viewModel.page, 1)
|
||||
XCTAssertEqual(api.taskListCalls.count, 1)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
|
||||
}
|
||||
|
||||
/// 测试任务列表首屏和加载更多会更新分页。
|
||||
func testTaskManagementReloadAndLoadMore() async throws {
|
||||
let api = TaskMockService()
|
||||
api.taskListResponses = [
|
||||
try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success"),
|
||||
try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_page2_success")
|
||||
]
|
||||
let viewModel = TaskManagementViewModel()
|
||||
|
||||
try await viewModel.reload(api: api, scenicId: 88)
|
||||
viewModel.total = 3
|
||||
try await viewModel.loadMore(api: api, scenicId: 88)
|
||||
|
||||
XCTAssertEqual(viewModel.tasks.count, 3)
|
||||
XCTAssertEqual(viewModel.page, 2)
|
||||
XCTAssertEqual(api.taskListCalls.map(\.page), [1, 2])
|
||||
}
|
||||
|
||||
/// 测试筛选参数会传给任务服务层。
|
||||
func testTaskManagementUsesFilters() async throws {
|
||||
let api = TaskMockService()
|
||||
api.taskListResponses = [try TestFixture.payload(ListPayload<PhotographerTaskItem>.self, named: "task_list_success")]
|
||||
let viewModel = TaskManagementViewModel()
|
||||
viewModel.selectedStatus = .accepted
|
||||
viewModel.searchText = " 亲子 "
|
||||
viewModel.startDate = Self.date("2026-06-01")
|
||||
viewModel.endDate = Self.date("2026-06-30")
|
||||
|
||||
try await viewModel.reload(api: api, scenicId: 88)
|
||||
|
||||
let call = try XCTUnwrap(api.taskListCalls.first)
|
||||
XCTAssertEqual(call.taskStatus, TaskStatusFilter.accepted.rawValue)
|
||||
XCTAssertEqual(call.taskName, "亲子")
|
||||
XCTAssertEqual(call.startTime, "2026-06-01")
|
||||
XCTAssertEqual(call.endTime, "2026-06-30")
|
||||
}
|
||||
|
||||
/// 测试任务详情加载成功后保存详情。
|
||||
func testTaskDetailLoadsDetail() async throws {
|
||||
let api = TaskMockService()
|
||||
api.detailResponse = try TestFixture.payload(TaskDetailResponse.self, named: "task_detail_success")
|
||||
let viewModel = TaskDetailViewModel()
|
||||
|
||||
await viewModel.load(api: api, taskId: 101)
|
||||
|
||||
XCTAssertEqual(api.detailIds, [101])
|
||||
XCTAssertEqual(viewModel.detail?.name, "游客精修任务")
|
||||
XCTAssertNil(viewModel.errorMessage)
|
||||
}
|
||||
|
||||
/// 测试发布任务校验必填项和景区。
|
||||
func testTaskCreateValidation() async throws {
|
||||
let api = TaskMockService()
|
||||
let upload = TaskMockUploadService()
|
||||
let viewModel = TaskCreateViewModel()
|
||||
|
||||
let noScenic = try await viewModel.submit(api: api, uploadService: upload, scenicId: nil)
|
||||
XCTAssertFalse(noScenic)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
|
||||
|
||||
viewModel.taskName = "任务"
|
||||
let noRemark = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
XCTAssertFalse(noRemark)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请输入任务备注")
|
||||
}
|
||||
|
||||
/// 测试发布任务会把云盘文件和本地上传文件写入请求。
|
||||
func testTaskCreateUploadsLocalFilesBeforeSubmit() async throws {
|
||||
let api = TaskMockService()
|
||||
let upload = TaskMockUploadService()
|
||||
upload.taskUploadURLs = ["https://cdn.example.com/task/a.jpg"]
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.taskName = "任务"
|
||||
viewModel.remark = "备注"
|
||||
viewModel.urgentHourText = "6"
|
||||
viewModel.mergeCloudFiles([TaskCloudSelectionItem(id: 501, fileName: "cloud.jpg", fileType: 2, remark: "")])
|
||||
viewModel.addLocalFile(data: Data([1, 2, 3]), fileName: "local.jpg")
|
||||
|
||||
let success = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(upload.taskUploads.count, 1)
|
||||
let request = try XCTUnwrap(api.addTaskRequests.first)
|
||||
XCTAssertEqual(request.scenicId, "88")
|
||||
XCTAssertEqual(request.name, "任务")
|
||||
XCTAssertEqual(request.cloudFile.first?.fileId, 501)
|
||||
XCTAssertEqual(request.uploadFile.first?.fileUrl, "https://cdn.example.com/task/a.jpg")
|
||||
XCTAssertTrue(viewModel.selectedLocalFiles.isEmpty)
|
||||
}
|
||||
|
||||
/// 测试本地文件上传失败时不会提交发布任务。
|
||||
func testTaskCreateUploadFailureStopsSubmit() async {
|
||||
let api = TaskMockService()
|
||||
let upload = TaskMockUploadService()
|
||||
upload.uploadError = APIError.networkFailed("上传失败")
|
||||
let viewModel = TaskCreateViewModel()
|
||||
viewModel.taskName = "任务"
|
||||
viewModel.remark = "备注"
|
||||
viewModel.addLocalFile(data: Data([1]), fileName: "local.jpg")
|
||||
|
||||
do {
|
||||
_ = try await viewModel.submit(api: api, uploadService: upload, scenicId: 88)
|
||||
XCTFail("Expected upload failure")
|
||||
} catch APIError.networkFailed(let message) {
|
||||
XCTAssertEqual(message, "上传失败")
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertTrue(api.addTaskRequests.isEmpty)
|
||||
XCTAssertEqual(viewModel.selectedLocalFiles.first?.errorMessage, "网络请求失败:上传失败")
|
||||
}
|
||||
|
||||
/// 测试云盘选择器进入文件夹、分页、选择文件和忽略文件夹选择。
|
||||
func testCloudSelectionNavigationAndSelection() async throws {
|
||||
let api = TaskMockService()
|
||||
let firstPage = try TestFixture.payload(ListPayload<CloudDriveFile>.self, named: "cloud_file_list_success")
|
||||
let secondPage = ListPayload(total: 3, list: [firstPage.list[1]])
|
||||
api.cloudFileResponses = [firstPage, secondPage, firstPage]
|
||||
let viewModel = TaskCloudFileSelectionViewModel()
|
||||
|
||||
try await viewModel.reload(api: api)
|
||||
viewModel.total = 3
|
||||
try await viewModel.loadMore(api: api)
|
||||
XCTAssertEqual(viewModel.files.count, 3)
|
||||
|
||||
let folder = firstPage.list[0]
|
||||
try await viewModel.enterFolder(folder, api: api)
|
||||
XCTAssertEqual(viewModel.currentFolderId, folder.id)
|
||||
|
||||
viewModel.toggleSelection(folder)
|
||||
XCTAssertTrue(viewModel.selectedFiles.isEmpty)
|
||||
|
||||
let file = firstPage.list[1]
|
||||
viewModel.toggleSelection(file)
|
||||
XCTAssertEqual(viewModel.makeSelectionItems().first?.id, file.id)
|
||||
XCTAssertEqual(api.cloudFileCalls.map(\.page), [1, 2, 1])
|
||||
}
|
||||
|
||||
/// 构造固定日期。
|
||||
private static func date(_ text: String) -> Date {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter.date(from: text)!
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 任务服务测试替身。
|
||||
private final class TaskMockService: TaskServing {
|
||||
struct TaskListCall {
|
||||
let scenicId: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
let taskStatus: Int
|
||||
let taskName: String?
|
||||
let startTime: String?
|
||||
let endTime: String?
|
||||
}
|
||||
|
||||
struct CloudFileCall {
|
||||
let parentFolderId: Int
|
||||
let name: String
|
||||
let type: Int
|
||||
let orderBy: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
}
|
||||
|
||||
var taskListResponses: [ListPayload<PhotographerTaskItem>] = []
|
||||
var detailResponse: TaskDetailResponse?
|
||||
var availableOrders: [AvailableOrderResponse] = []
|
||||
var cloudFileResponses: [ListPayload<CloudDriveFile>] = []
|
||||
private(set) var taskListCalls: [TaskListCall] = []
|
||||
private(set) var detailIds: [Int] = []
|
||||
private(set) var availableOrderScenicIds: [Int] = []
|
||||
private(set) var addTaskRequests: [AddTaskRequest] = []
|
||||
private(set) var cloudFileCalls: [CloudFileCall] = []
|
||||
|
||||
/// 记录任务列表请求并返回下一份响应。
|
||||
func taskList(
|
||||
scenicId: Int,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
taskStatus: Int,
|
||||
taskName: String?,
|
||||
startTime: String?,
|
||||
endTime: String?
|
||||
) async throws -> ListPayload<PhotographerTaskItem> {
|
||||
taskListCalls.append(
|
||||
TaskListCall(
|
||||
scenicId: scenicId,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
taskStatus: taskStatus,
|
||||
taskName: taskName,
|
||||
startTime: startTime,
|
||||
endTime: endTime
|
||||
)
|
||||
)
|
||||
return taskListResponses.isEmpty ? ListPayload(total: 0, list: []) : taskListResponses.removeFirst()
|
||||
}
|
||||
|
||||
/// 记录任务详情请求并返回响应。
|
||||
func taskDetail(id: Int) async throws -> TaskDetailResponse {
|
||||
detailIds.append(id)
|
||||
return detailResponse!
|
||||
}
|
||||
|
||||
/// 记录可关联订单请求并返回响应。
|
||||
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
|
||||
availableOrderScenicIds.append(scenicId)
|
||||
return availableOrders
|
||||
}
|
||||
|
||||
/// 记录发布任务请求。
|
||||
func addTask(_ request: AddTaskRequest) async throws {
|
||||
addTaskRequests.append(request)
|
||||
}
|
||||
|
||||
/// 记录云盘文件请求并返回下一份响应。
|
||||
func cloudFileList(
|
||||
parentFolderId: Int,
|
||||
name: String,
|
||||
type: Int,
|
||||
orderBy: Int,
|
||||
page: Int,
|
||||
pageSize: Int
|
||||
) async throws -> ListPayload<CloudDriveFile> {
|
||||
cloudFileCalls.append(
|
||||
CloudFileCall(
|
||||
parentFolderId: parentFolderId,
|
||||
name: name,
|
||||
type: type,
|
||||
orderBy: orderBy,
|
||||
page: page,
|
||||
pageSize: pageSize
|
||||
)
|
||||
)
|
||||
return cloudFileResponses.isEmpty ? ListPayload(total: 0, list: []) : cloudFileResponses.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// OSS 上传服务测试替身。
|
||||
private final class TaskMockUploadService: OSSUploadServing {
|
||||
struct TaskUpload {
|
||||
let data: Data
|
||||
let fileName: String
|
||||
let fileType: Int
|
||||
let scenicId: Int
|
||||
}
|
||||
|
||||
var taskUploadURLs: [String] = []
|
||||
var uploadError: Error?
|
||||
private(set) var taskUploads: [TaskUpload] = []
|
||||
|
||||
/// 模拟上传任务附件。
|
||||
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
taskUploads.append(TaskUpload(data: data, fileName: fileName, fileType: fileType, scenicId: scenicId))
|
||||
onProgress(50)
|
||||
if let uploadError {
|
||||
throw uploadError
|
||||
}
|
||||
onProgress(100)
|
||||
return taskUploadURLs.isEmpty ? "https://cdn.example.com/task/default.jpg" : taskUploadURLs.removeFirst()
|
||||
}
|
||||
|
||||
/// 模拟上传头像。
|
||||
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 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 { "" }
|
||||
}
|
||||
Reference in New Issue
Block a user