335 lines
12 KiB
Swift
335 lines
12 KiB
Swift
//
|
|
// ProfileSpaceSettingsViewModelTests.swift
|
|
// suixinkanTests
|
|
//
|
|
|
|
import XCTest
|
|
@testable import suixinkan
|
|
|
|
@MainActor
|
|
/// 个人空间配置 ViewModel 与首页路由测试。
|
|
final class ProfileSpaceSettingsViewModelTests: XCTestCase {
|
|
|
|
private var defaults: UserDefaults!
|
|
private var appStore: AppStore!
|
|
private var calendar: Calendar!
|
|
private var fixedNow: Date!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
defaults = UserDefaults(suiteName: "ProfileSpaceSettingsViewModelTests")!
|
|
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
|
|
appStore = AppStore(defaults: defaults)
|
|
appStore.currentScenicId = 11
|
|
calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
fixedNow = calendar.date(from: DateComponents(year: 2026, month: 7, day: 8))!
|
|
}
|
|
|
|
override func tearDown() {
|
|
AppStore.shared.currentScenicId = 0
|
|
appStore.logout()
|
|
defaults.removePersistentDomain(forName: "ProfileSpaceSettingsViewModelTests")
|
|
super.tearDown()
|
|
}
|
|
|
|
func testProfileInfoDecodeSupportsEmptyStringsArraysAndScheduleMap() throws {
|
|
let json = """
|
|
{
|
|
"id": "9",
|
|
"scenic_id": "11",
|
|
"photog_uid": "",
|
|
"real_name": "",
|
|
"nickname": "小林",
|
|
"avatar": "",
|
|
"scenic_certification": ["西湖", ""],
|
|
"description": "",
|
|
"attr_label": ["航拍", 8],
|
|
"shoot_label": "",
|
|
"camera_device": ["Sony A7"],
|
|
"business_start_time": "09:00:00",
|
|
"business_end_time": "18:00:00",
|
|
"holiday_start_time": "",
|
|
"holiday_end_time": "20:00:00",
|
|
"business_range": ["1", 2],
|
|
"accept_order_status": "1",
|
|
"virtual_phone": "",
|
|
"schedule": {
|
|
"2026-07-08": [{
|
|
"id": "22",
|
|
"name": "旅拍",
|
|
"remark": "",
|
|
"start_time": "10:00",
|
|
"end_time": "11:00",
|
|
"schedule_date": "2026-07-08",
|
|
"order_number": "NO1",
|
|
"user_nickname": "游客",
|
|
"user_phone": "13800000000",
|
|
"order_status": "2",
|
|
"order_status_name": "待拍摄"
|
|
}]
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let response = try JSONDecoder().decode(PhotographerProfileInfoResponse.self, from: json)
|
|
|
|
XCTAssertEqual(response.id, 9)
|
|
XCTAssertEqual(response.photogUid, 0)
|
|
XCTAssertEqual(response.attrLabel, ["航拍", "8"])
|
|
XCTAssertEqual(response.shootLabel, [])
|
|
XCTAssertEqual(response.businessRange, [1, 2])
|
|
XCTAssertEqual(response.schedule["2026-07-08"]?.first?.id, 22)
|
|
XCTAssertEqual(response.schedule["2026-07-08"]?.first?.userPhone, "13800000000")
|
|
}
|
|
|
|
func testLabelValidationCoversEmptyTooLongDuplicateAndLimit() {
|
|
let viewModel = makeViewModel()
|
|
var messages: [String] = []
|
|
viewModel.onShowMessage = { messages.append($0) }
|
|
|
|
viewModel.addAttrLabel(" ")
|
|
XCTAssertEqual(messages.last, "标签不能为空")
|
|
|
|
viewModel.addAttrLabel(String(repeating: "a", count: 21))
|
|
XCTAssertEqual(messages.last, "标签最多20个字符")
|
|
|
|
viewModel.addAttrLabel("航拍")
|
|
viewModel.addAttrLabel("航拍")
|
|
XCTAssertEqual(viewModel.attrLabels, ["航拍"])
|
|
XCTAssertEqual(messages.last, "标签已存在")
|
|
|
|
for index in 1...7 {
|
|
viewModel.addAttrLabel("标签\(index)")
|
|
}
|
|
viewModel.addAttrLabel("第九个")
|
|
XCTAssertEqual(viewModel.attrLabels.count, 8)
|
|
XCTAssertEqual(messages.last, "最多8个标签")
|
|
}
|
|
|
|
func testBusinessTimeValidationRejectsInvalidRanges() throws {
|
|
let viewModel = makeViewModel()
|
|
var messages: [String] = []
|
|
viewModel.onShowMessage = { messages.append($0) }
|
|
|
|
viewModel.updateBusinessEndTime("10:00")
|
|
viewModel.updateBusinessStartTime("11:00")
|
|
XCTAssertNil(viewModel.businessStartTime)
|
|
XCTAssertEqual(messages.last, "开始时间不能大于结束时间")
|
|
|
|
viewModel.updateHolidayStartTime("13:00")
|
|
viewModel.updateHolidayEndTime("12:00")
|
|
XCTAssertNil(viewModel.holidayEndTime)
|
|
XCTAssertEqual(messages.last, "结束时间不能小于开始时间")
|
|
|
|
viewModel.updateBusinessStartTime("09:00")
|
|
viewModel.updateBusinessEndTime("18:00")
|
|
viewModel.updateHolidayStartTime("08:00")
|
|
viewModel.updateHolidayEndTime("20:00")
|
|
|
|
viewModel.updateBusinessEndTime("08:00")
|
|
XCTAssertThrowsError(try viewModel.makeSaveRequest())
|
|
}
|
|
|
|
func testHasChangesAndSaveRequestFieldsMatchAndroid() async throws {
|
|
let api = FakeProfileSpaceSettingsAPI(response: .loaded())
|
|
let viewModel = makeViewModel()
|
|
|
|
await viewModel.load(api: api)
|
|
XCTAssertFalse(viewModel.hasChanges)
|
|
|
|
viewModel.updateIntroduction("更新后的简介")
|
|
viewModel.addDevice("Nikon Z8")
|
|
viewModel.updateAcceptOrderStatus(2)
|
|
XCTAssertTrue(viewModel.hasChanges)
|
|
|
|
await viewModel.save(api: api)
|
|
|
|
let request = try XCTUnwrap(api.lastUpdateRequest)
|
|
XCTAssertEqual(request.scenicId, "11")
|
|
XCTAssertEqual(request.description, "更新后的简介")
|
|
XCTAssertEqual(request.cameraDevice, ["Sony A7", "Nikon Z8"])
|
|
XCTAssertEqual(request.attrLabel, ["航拍"])
|
|
XCTAssertEqual(request.shootLabel, ["亲子"])
|
|
XCTAssertEqual(request.businessStartTime, "09:00:00")
|
|
XCTAssertEqual(request.businessEndTime, "18:00:00")
|
|
XCTAssertEqual(request.holidayStartTime, "10:00:00")
|
|
XCTAssertEqual(request.holidayEndTime, "20:00:00")
|
|
XCTAssertEqual(request.businessRange, [])
|
|
XCTAssertEqual(request.acceptOrderStatus, 2)
|
|
XCTAssertFalse(viewModel.hasChanges)
|
|
}
|
|
|
|
func testAddScheduleValidationAndDeleteId() async throws {
|
|
let api = FakeProfileSpaceSettingsAPI(response: .loaded())
|
|
let viewModel = makeViewModel()
|
|
let scheduleDate = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 7, day: 9)))
|
|
|
|
XCTAssertThrowsError(try viewModel.makeAddScheduleRequest(
|
|
name: "",
|
|
remark: "内容",
|
|
startTime: "10:00",
|
|
endTime: "11:00",
|
|
scheduleDate: scheduleDate,
|
|
orderNumber: nil
|
|
)) { error in
|
|
XCTAssertEqual(error as? ProfileSpaceSettingsValidationError, .emptyScheduleName)
|
|
}
|
|
|
|
XCTAssertThrowsError(try viewModel.makeAddScheduleRequest(
|
|
name: "日程",
|
|
remark: "内容",
|
|
startTime: "12:00",
|
|
endTime: "11:00",
|
|
scheduleDate: scheduleDate,
|
|
orderNumber: nil
|
|
)) { error in
|
|
XCTAssertEqual(error as? ProfileSpaceSettingsValidationError, .invalidScheduleTime)
|
|
}
|
|
|
|
let didAdd = await viewModel.addSchedule(
|
|
name: " 日程 ",
|
|
remark: "内容",
|
|
startTime: "10:00",
|
|
endTime: "11:00",
|
|
scheduleDate: scheduleDate,
|
|
orderNumber: " ",
|
|
api: api
|
|
)
|
|
|
|
XCTAssertTrue(didAdd)
|
|
XCTAssertEqual(api.lastAddScheduleRequest?.scenicId, "11")
|
|
XCTAssertEqual(api.lastAddScheduleRequest?.name, "日程")
|
|
XCTAssertEqual(api.lastAddScheduleRequest?.scheduleDate, "2026-07-09")
|
|
XCTAssertNil(api.lastAddScheduleRequest?.orderNumber)
|
|
|
|
await viewModel.deleteSchedule(id: 33, api: api)
|
|
XCTAssertEqual(api.lastDeletedScheduleId, 33)
|
|
}
|
|
|
|
func testSpaceSettingsRoutePushesProfileSpaceSettingsPage() {
|
|
AppStore.shared.currentScenicId = 11
|
|
let root = UIViewController()
|
|
let navigationController = UINavigationController(rootViewController: root)
|
|
|
|
HomeRouteHandler.open(uri: "space_settings", from: root, storeItem: nil)
|
|
|
|
XCTAssertTrue(navigationController.topViewController is ProfileSpaceSettingsViewController)
|
|
}
|
|
|
|
func testSystemSettingsRouteStillSelectsProfileTab() {
|
|
AppStore.shared.currentScenicId = 11
|
|
let tabBarController = MainTabBarController()
|
|
tabBarController.loadViewIfNeeded()
|
|
guard
|
|
let navigationController = tabBarController.selectedViewController as? UINavigationController,
|
|
let root = navigationController.viewControllers.first
|
|
else {
|
|
XCTFail("首页 Tab 未正确初始化")
|
|
return
|
|
}
|
|
|
|
HomeRouteHandler.open(uri: "system_settings", from: root, storeItem: nil)
|
|
|
|
XCTAssertEqual(tabBarController.selectedIndex, 4)
|
|
}
|
|
|
|
private func makeViewModel() -> ProfileSpaceSettingsViewModel {
|
|
ProfileSpaceSettingsViewModel(
|
|
appStore: appStore,
|
|
calendar: calendar,
|
|
nowProvider: { self.fixedNow }
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class FakeProfileSpaceSettingsAPI: ProfileSpaceSettingsAPI {
|
|
var response: PhotographerProfileInfoResponse
|
|
var lastUpdateRequest: UpdatePhotographerProfileInfoRequest?
|
|
var lastAddScheduleRequest: AddPhotographerScheduleRequest?
|
|
var lastDeletedScheduleId: Int?
|
|
|
|
init(response: PhotographerProfileInfoResponse) {
|
|
self.response = response
|
|
}
|
|
|
|
func profileInfo(scenicId: Int) async throws -> PhotographerProfileInfoResponse {
|
|
response
|
|
}
|
|
|
|
func updateProfileInfo(_ request: UpdatePhotographerProfileInfoRequest) async throws {
|
|
lastUpdateRequest = request
|
|
response = PhotographerProfileInfoResponse(
|
|
id: response.id,
|
|
scenicId: Int(request.scenicId) ?? 0,
|
|
photogUid: response.photogUid,
|
|
realName: response.realName,
|
|
nickname: response.nickname,
|
|
avatar: response.avatar,
|
|
scenicCertification: response.scenicCertification,
|
|
description: request.description,
|
|
attrLabel: request.attrLabel,
|
|
shootLabel: request.shootLabel,
|
|
cameraDevice: request.cameraDevice,
|
|
businessStartTime: request.businessStartTime,
|
|
businessEndTime: request.businessEndTime,
|
|
holidayStartTime: request.holidayStartTime,
|
|
holidayEndTime: request.holidayEndTime,
|
|
businessRange: request.businessRange,
|
|
acceptOrderStatus: request.acceptOrderStatus,
|
|
schedule: response.schedule,
|
|
virtualPhone: response.virtualPhone
|
|
)
|
|
}
|
|
|
|
func updateUserInfo(nickname: String?, password: String?, avatar: String?) async throws {}
|
|
|
|
func addSchedule(_ request: AddPhotographerScheduleRequest) async throws {
|
|
lastAddScheduleRequest = request
|
|
}
|
|
|
|
func deleteSchedule(id: Int) async throws {
|
|
lastDeletedScheduleId = id
|
|
}
|
|
}
|
|
|
|
private extension PhotographerProfileInfoResponse {
|
|
static func loaded() -> PhotographerProfileInfoResponse {
|
|
PhotographerProfileInfoResponse(
|
|
id: 1,
|
|
scenicId: 11,
|
|
photogUid: 100,
|
|
realName: "林摄影",
|
|
nickname: "小林",
|
|
avatar: "https://example.com/a.jpg",
|
|
scenicCertification: ["西湖"],
|
|
description: "简介",
|
|
attrLabel: ["航拍"],
|
|
shootLabel: ["亲子"],
|
|
cameraDevice: ["Sony A7"],
|
|
businessStartTime: "09:00:00",
|
|
businessEndTime: "18:00:00",
|
|
holidayStartTime: "10:00:00",
|
|
holidayEndTime: "20:00:00",
|
|
businessRange: [],
|
|
acceptOrderStatus: 1,
|
|
schedule: [
|
|
"2026-07-08": [
|
|
PhotographerSchedule(
|
|
id: 22,
|
|
name: "旅拍",
|
|
remark: "西湖",
|
|
startTime: "10:00",
|
|
endTime: "11:00",
|
|
scheduleDate: "2026-07-08",
|
|
userNickname: "游客",
|
|
userPhone: "13800000000"
|
|
),
|
|
],
|
|
],
|
|
virtualPhone: ""
|
|
)
|
|
}
|
|
}
|