185 lines
7.4 KiB
Swift
185 lines
7.4 KiB
Swift
//
|
|
// ScheduleTests.swift
|
|
// suixinkanTests
|
|
//
|
|
// Created by Codex on 2026/6/24.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import suixinkan_ios
|
|
|
|
@MainActor
|
|
/// 排班 API 和 ViewModel 测试。
|
|
final class ScheduleTests: XCTestCase {
|
|
/// 测试排班 API path 和 query/body。
|
|
func testScheduleAPIUsesExpectedRequests() async throws {
|
|
let session = ScheduleRecordingURLSession(responses: [
|
|
Data(#"{"code":100000,"message":"ok","data":["2026-06-24"]}"#.utf8),
|
|
Data(#"{"code":100000,"message":"ok","data":[{"id":"1","name":"拍摄","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}]}"#.utf8),
|
|
Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8),
|
|
Data(#"{"code":100000,"message":"ok","data":{}}"#.utf8)
|
|
])
|
|
let api = ScheduleAPI(client: APIClient(session: session))
|
|
|
|
let days = try await api.monthScheduleDays(scenicId: 9, yearMonth: "2026-06")
|
|
let list = try await api.dayScheduleList(scenicId: 9, date: "2026-06-24")
|
|
try await api.addSchedule(AddScheduleRequest(scenicId: "9", name: "拍摄", remark: "备注", startTime: "09:00", endTime: "10:00", scheduleDate: "2026-06-24", orderNumber: "ORD001"))
|
|
try await api.deleteSchedule(id: 1)
|
|
|
|
XCTAssertEqual(days, ["2026-06-24"])
|
|
XCTAssertEqual(list.first?.id, 1)
|
|
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
|
"/api/yf-handset-app/photog/schedule/list-date",
|
|
"/api/yf-handset-app/photog/schedule/list",
|
|
"/api/yf-handset-app/photog/schedule/add",
|
|
"/api/yf-handset-app/photog/schedule/delete"
|
|
])
|
|
XCTAssertEqual(scheduleQueryItems(from: session.requests[0])["date"], "2026-06")
|
|
XCTAssertEqual(scheduleQueryItems(from: session.requests[1])["date"], "2026-06-24")
|
|
XCTAssertEqual((try scheduleBodyObject(from: session.requests[2]))["order_number"] as? String, "ORD001")
|
|
XCTAssertEqual((try scheduleBodyObject(from: session.requests[3]))["id"] as? Int, 1)
|
|
}
|
|
|
|
/// 测试无景区时排班管理清空状态且不请求接口。
|
|
func testScheduleManagementWithoutScenicClearsState() async {
|
|
let api = FakeScheduleService()
|
|
let viewModel = ScheduleManagementViewModel()
|
|
|
|
await viewModel.reload(api: api, scenicId: nil)
|
|
|
|
XCTAssertTrue(viewModel.items.isEmpty)
|
|
XCTAssertTrue(viewModel.markedDays.isEmpty)
|
|
XCTAssertEqual(api.monthCalls.count, 0)
|
|
}
|
|
|
|
/// 测试排班管理加载月份和当天列表。
|
|
func testScheduleManagementReloadLoadsMonthAndDay() async {
|
|
let api = FakeScheduleService()
|
|
api.monthDays = ["2026-06-24"]
|
|
api.dayItems = [.fixture(id: 1)]
|
|
let viewModel = ScheduleManagementViewModel()
|
|
|
|
await viewModel.reload(api: api, scenicId: 9)
|
|
|
|
XCTAssertEqual(api.monthCalls.first?.scenicId, 9)
|
|
XCTAssertEqual(api.dayCalls.first?.scenicId, 9)
|
|
XCTAssertEqual(viewModel.markedDays, ["2026-06-24"])
|
|
XCTAssertEqual(viewModel.items.first?.id, 1)
|
|
}
|
|
|
|
/// 测试新增排班优先使用选择的订单号。
|
|
func testScheduleAddUsesSelectedOrderBeforeManualOrder() async {
|
|
let api = FakeScheduleService()
|
|
let viewModel = ScheduleAddViewModel()
|
|
viewModel.draft.name = "拍摄"
|
|
viewModel.draft.remark = ""
|
|
viewModel.draft.manualOrderNumber = "MANUAL"
|
|
viewModel.draft.selectedOrder = AvailableOrderResponse.fixture(orderNumber: "PICKED")
|
|
let start = fixedDate(hour: 9)
|
|
let end = fixedDate(hour: 10)
|
|
|
|
let success = await viewModel.submit(api: api, scenicId: 9, startDate: start, endDate: end)
|
|
|
|
XCTAssertTrue(success)
|
|
XCTAssertEqual(api.addedRequests.first?.orderNumber, "PICKED")
|
|
XCTAssertEqual(api.addedRequests.first?.remark, "拍摄")
|
|
}
|
|
|
|
/// 测试结束时间早于开始时间时禁止提交。
|
|
func testScheduleAddRejectsInvalidTime() async {
|
|
let api = FakeScheduleService()
|
|
let viewModel = ScheduleAddViewModel()
|
|
viewModel.draft.name = "拍摄"
|
|
|
|
let success = await viewModel.submit(api: api, scenicId: 9, startDate: fixedDate(hour: 10), endDate: fixedDate(hour: 9))
|
|
|
|
XCTAssertFalse(success)
|
|
XCTAssertTrue(api.addedRequests.isEmpty)
|
|
XCTAssertEqual(viewModel.errorMessage, ScheduleValidationError.invalidTime.localizedDescription)
|
|
}
|
|
|
|
private func fixedDate(hour: Int) -> Date {
|
|
Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: hour, minute: 0)) ?? Date()
|
|
}
|
|
}
|
|
|
|
private extension ScheduleItem {
|
|
/// 创建排班测试项。
|
|
static func fixture(id: Int) -> ScheduleItem {
|
|
let data = Data(#"{"id":\#(id),"name":"拍摄","start_time":"09:00","end_time":"10:00","schedule_date":"2026-06-24"}"#.utf8)
|
|
return try! JSONDecoder().decode(ScheduleItem.self, from: data)
|
|
}
|
|
}
|
|
|
|
private extension AvailableOrderResponse {
|
|
/// 创建可关联订单测试项。
|
|
static func fixture(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)
|
|
}
|
|
}
|
|
|
|
/// 排班 API 测试 URLSession。
|
|
private final class ScheduleRecordingURLSession: URLSessionProtocol {
|
|
private var responses: [Data]
|
|
private(set) var requests: [URLRequest] = []
|
|
|
|
/// 初始化测试 Session。
|
|
init(responses: [Data]) {
|
|
self.responses = responses
|
|
}
|
|
|
|
/// 记录请求并返回响应。
|
|
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
|
requests.append(request)
|
|
let data = responses.removeFirst()
|
|
return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
|
|
}
|
|
}
|
|
|
|
/// 排班服务测试替身。
|
|
@MainActor
|
|
private final class FakeScheduleService: ScheduleServing {
|
|
var monthDays: [String] = []
|
|
var dayItems: [ScheduleItem] = []
|
|
var monthCalls: [(scenicId: Int, yearMonth: String)] = []
|
|
var dayCalls: [(scenicId: Int, date: String)] = []
|
|
var addedRequests: [AddScheduleRequest] = []
|
|
|
|
func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String] {
|
|
monthCalls.append((scenicId, yearMonth))
|
|
return monthDays
|
|
}
|
|
|
|
func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem] {
|
|
dayCalls.append((scenicId, date))
|
|
return dayItems
|
|
}
|
|
|
|
func addSchedule(_ request: AddScheduleRequest) async throws {
|
|
addedRequests.append(request)
|
|
}
|
|
|
|
func deleteSchedule(id: Int) async throws {}
|
|
|
|
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
|
|
[]
|
|
}
|
|
}
|
|
|
|
/// 从请求中提取 query 字典。
|
|
private func scheduleQueryItems(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) }
|
|
})
|
|
}
|
|
|
|
/// 将请求体解析为字典。
|
|
private func scheduleBodyObject(from request: URLRequest) throws -> [String: Any] {
|
|
let body = try XCTUnwrap(request.httpBody)
|
|
return try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
|
|
}
|