同步个人空间配置页面
This commit is contained in:
334
suixinkanTests/ProfileSpaceSettingsViewModelTests.swift
Normal file
334
suixinkanTests/ProfileSpaceSettingsViewModelTests.swift
Normal file
@ -0,0 +1,334 @@
|
||||
//
|
||||
// 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: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user