Add scenic selection and cooperation order flows aligned with Android.
Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
86
suixinkanTests/BindAcquirerViewModelTests.swift
Normal file
86
suixinkanTests/BindAcquirerViewModelTests.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// BindAcquirerViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
final class BindAcquirerViewModelTests: XCTestCase {
|
||||
|
||||
func testAcceptBindValidationRequiresRate() async {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.initSaleUserId(1)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.acceptBind(api: makeAPI(responses: []))
|
||||
|
||||
XCTAssertEqual(message, "请输入分账比例")
|
||||
}
|
||||
|
||||
func testAcceptBindValidationRejectsOutOfRange() async {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.initSaleUserId(1)
|
||||
viewModel.updateCommissionRateDraft("120")
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.acceptBind(api: makeAPI(responses: []))
|
||||
|
||||
XCTAssertEqual(message, "分账比例需在0到100之间")
|
||||
}
|
||||
|
||||
func testLoadInviteInfoMarksAlreadyBound() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{"sale_user_id":7,"is_bound":true,"saler_name":"张三","saler_phone":"13800138000","title":"接受邀请"}}
|
||||
""".data(using: .utf8)!
|
||||
let api = makeAPI(responses: [json])
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.initSaleUserId(7)
|
||||
|
||||
await viewModel.loadInviteInfo(api: api)
|
||||
|
||||
XCTAssertTrue(viewModel.isAlreadyBound)
|
||||
XCTAssertEqual(viewModel.inviteInfo?.salerName, "张三")
|
||||
}
|
||||
|
||||
func testAcceptBindSuccessCallsCallback() async throws {
|
||||
let inviteJSON = """
|
||||
{"code":100000,"msg":"success","data":{"sale_user_id":2,"saler_name":"李四"}}
|
||||
""".data(using: .utf8)!
|
||||
let bindJSON = """
|
||||
{"code":100000,"msg":"success","data":{}}
|
||||
""".data(using: .utf8)!
|
||||
let api = makeAPI(responses: [inviteJSON, bindJSON])
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.initSaleUserId(2)
|
||||
viewModel.updateCommissionRateDraft("30")
|
||||
var didBind = false
|
||||
viewModel.onBindSuccess = { didBind = true }
|
||||
|
||||
await viewModel.loadInviteInfo(api: api)
|
||||
await viewModel.acceptBind(api: api)
|
||||
|
||||
XCTAssertTrue(didBind)
|
||||
}
|
||||
|
||||
func testLoadInviteInfoAlreadyBoundMessage() async throws {
|
||||
let json = try TestJSON.errorEnvelope(code: 100001, msg: "已绑定获客员")
|
||||
let api = makeAPI(responses: [json])
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.initSaleUserId(9)
|
||||
var shouldPop = false
|
||||
viewModel.onShouldPop = { shouldPop = true }
|
||||
|
||||
await viewModel.loadInviteInfo(api: api)
|
||||
|
||||
XCTAssertTrue(viewModel.isAlreadyBound)
|
||||
XCTAssertFalse(shouldPop)
|
||||
}
|
||||
|
||||
private func makeAPI(responses: [Data]) -> OrderAPI {
|
||||
OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: responses)))
|
||||
}
|
||||
}
|
||||
88
suixinkanTests/CooperationAcquirerViewModelTests.swift
Normal file
88
suixinkanTests/CooperationAcquirerViewModelTests.swift
Normal file
@ -0,0 +1,88 @@
|
||||
//
|
||||
// CooperationAcquirerViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
final class CooperationAcquirerViewModelTests: XCTestCase {
|
||||
|
||||
private var appStore: AppStore!
|
||||
private var defaults: UserDefaults!
|
||||
|
||||
override func setUp() {
|
||||
defaults = UserDefaults(suiteName: "CooperationAcquirerViewModelTests")!
|
||||
defaults.removePersistentDomain(forName: "CooperationAcquirerViewModelTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
appStore.currentStoreId = 5
|
||||
}
|
||||
|
||||
func testSaveRemarkWithValidLengthCallsAPI() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{}}
|
||||
""".data(using: .utf8)!
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
viewModel.startEditRemark(CooperativeSalerEntity(saleUserId: 1))
|
||||
viewModel.updateRemarkDraft("备注A")
|
||||
|
||||
await viewModel.saveRemark(api: makeAPI(responses: [json]))
|
||||
|
||||
XCTAssertEqual(message, "备注已保存")
|
||||
}
|
||||
|
||||
func testSaveCommissionRateRequiresSmsCode() async {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
viewModel.startEditCommissionRate(CooperativeSalerEntity(saleUserId: 1, salerPhone: "13800138000"))
|
||||
viewModel.updateCommissionRateDraft("20")
|
||||
|
||||
await viewModel.saveCommissionRate(api: makeAPI(responses: []))
|
||||
|
||||
XCTAssertEqual(message, "请输入验证码")
|
||||
}
|
||||
|
||||
func testProcessScanResultNavigatesWithValidQR() {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var navigatedId: Int?
|
||||
viewModel.onNavigateBindAcquirer = { navigatedId = $0 }
|
||||
|
||||
viewModel.processScanResult(#"{"method":"sale_user_bind","sale_user_id":88}"#)
|
||||
|
||||
XCTAssertEqual(navigatedId, 88)
|
||||
}
|
||||
|
||||
func testProcessScanResultShowsErrorForInvalidQR() {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var message: String?
|
||||
viewModel.onNavigateBindAcquirer = { _ in XCTFail("Should not navigate") }
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
viewModel.processScanResult("invalid")
|
||||
|
||||
XCTAssertEqual(message, "请扫描正确的获客员二维码")
|
||||
}
|
||||
|
||||
func testLoadAcquirersUsesStoreId() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"sale_user_id":3,"name":"获客员A"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
|
||||
await viewModel.loadAcquirers(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.acquirers.count, 1)
|
||||
let url = try XCTUnwrap(session.requests.first?.url?.absoluteString)
|
||||
XCTAssertTrue(url.contains("store_id=5"))
|
||||
}
|
||||
|
||||
private func makeAPI(responses: [Data]) -> OrderAPI {
|
||||
OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: responses)))
|
||||
}
|
||||
}
|
||||
63
suixinkanTests/CooperationOrderListViewModelTests.swift
Normal file
63
suixinkanTests/CooperationOrderListViewModelTests.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// CooperationOrderListViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
final class CooperationOrderListViewModelTests: XCTestCase {
|
||||
|
||||
func testRefreshLeadsLoadsFirstPage() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":1,"user_phone":"13800138000","created_at":"2026-01-01"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [json])))
|
||||
let viewModel = CooperationOrderListViewModel()
|
||||
|
||||
await viewModel.refreshLeads(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.leads.count, 1)
|
||||
XCTAssertFalse(viewModel.canLoadMore)
|
||||
XCTAssertFalse(viewModel.isRefreshing)
|
||||
}
|
||||
|
||||
func testSearchUsesKeywordQuery() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{"total":0,"list":[]}}
|
||||
""".data(using: .utf8)!
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = CooperationOrderListViewModel()
|
||||
viewModel.onTabSelected(1)
|
||||
viewModel.onSearchTextChange("张三")
|
||||
|
||||
await viewModel.refreshAcquisitionOrders(api: api)
|
||||
|
||||
let url = try XCTUnwrap(session.requests.first?.url?.absoluteString)
|
||||
XCTAssertTrue(url.contains("acquisition-orders"))
|
||||
XCTAssertTrue(url.contains("keyword="))
|
||||
}
|
||||
|
||||
func testLoadMoreAppendsLeads() async throws {
|
||||
let page1 = """
|
||||
{"code":100000,"msg":"success","data":{"total":2,"list":[{"id":1,"user_phone":"13800138000"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let page2 = """
|
||||
{"code":100000,"msg":"success","data":{"total":2,"list":[{"id":2,"user_phone":"13800138001"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [page1, page2])))
|
||||
let viewModel = CooperationOrderListViewModel()
|
||||
|
||||
await viewModel.refreshLeads(api: api)
|
||||
await viewModel.loadMore(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.leads.count, 2)
|
||||
}
|
||||
|
||||
func testShouldLoadMoreNearEnd() {
|
||||
let viewModel = CooperationOrderListViewModel()
|
||||
XCTAssertFalse(viewModel.shouldLoadMore(at: 0))
|
||||
}
|
||||
}
|
||||
68
suixinkanTests/CooperationOrderModelsTests.swift
Normal file
68
suixinkanTests/CooperationOrderModelsTests.swift
Normal file
@ -0,0 +1,68 @@
|
||||
//
|
||||
// CooperationOrderModelsTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 合作订单模型与校验测试。
|
||||
final class CooperationOrderModelsTests: XCTestCase {
|
||||
|
||||
func testRemarkNameValidationAllowsTenCharacters() {
|
||||
XCTAssertNil(CooperationOrderFeature.remarkNameValidationMessage("1234567890"))
|
||||
}
|
||||
|
||||
func testRemarkNameValidationRejectsOverTenCharacters() {
|
||||
XCTAssertEqual(
|
||||
CooperationOrderFeature.remarkNameValidationMessage("12345678901"),
|
||||
"备注名不能超过10个字符"
|
||||
)
|
||||
}
|
||||
|
||||
func testReferralLeadDisplayFields() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": 1,
|
||||
"lead_id": 9,
|
||||
"display_name": "展示名",
|
||||
"user_mobile_masked": "138****0000",
|
||||
"remark": "备注",
|
||||
"created_at": "2026-01-01",
|
||||
"image_urls": ["https://img/1.jpg"]
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let lead = try JSONDecoder().decode(ReferralLeadEntity.self, from: json)
|
||||
XCTAssertEqual(lead.displayId, "9")
|
||||
XCTAssertEqual(lead.displayPhone, "138****0000")
|
||||
XCTAssertEqual(lead.displaySalerName, "展示名")
|
||||
XCTAssertEqual(lead.displayImages, ["https://img/1.jpg"])
|
||||
}
|
||||
|
||||
func testAcquisitionOrderDisplayFields() throws {
|
||||
let json = """
|
||||
{
|
||||
"project_name": "跟拍套餐",
|
||||
"order_type_label": "摄影师跟拍",
|
||||
"order_number": "NO001",
|
||||
"photog_remark_name": "备注名",
|
||||
"sale_user_name": "获客员",
|
||||
"created_at": "2026-01-02",
|
||||
"user_mobile_masked": "139****1111",
|
||||
"order_amount": "99.00"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let order = try JSONDecoder().decode(AcquisitionOrderEntity.self, from: json)
|
||||
XCTAssertEqual(order.displayPartner, "备注名")
|
||||
XCTAssertEqual(order.displayCustomerPhone, "139****1111")
|
||||
XCTAssertEqual(order.displayAmount, "99.00")
|
||||
}
|
||||
|
||||
func testCooperativeSalerDisplayCommissionRateUsesLabelFirst() {
|
||||
let entity = CooperativeSalerEntity(
|
||||
commissionRate: 10,
|
||||
commissionRateLabel: "10%(固定)"
|
||||
)
|
||||
XCTAssertEqual(entity.displayCommissionRate, "10%(固定)")
|
||||
}
|
||||
}
|
||||
@ -148,4 +148,27 @@ final class HomeViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["wallet"])
|
||||
XCTAssertEqual(session.requests.count, 1)
|
||||
}
|
||||
|
||||
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
|
||||
appStore.currentScenicId = 10
|
||||
appStore.currentScenicName = ""
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
viewModel.refreshLocalDisplayState()
|
||||
|
||||
XCTAssertEqual(viewModel.currentScenicName, "")
|
||||
XCTAssertEqual(appStore.currentScenicId, 0)
|
||||
XCTAssertEqual(appStore.currentScenicName, "")
|
||||
}
|
||||
|
||||
func testRefreshLocalDisplayStateClearsWhenIdMissing() {
|
||||
appStore.currentScenicId = 0
|
||||
appStore.currentScenicName = "残留名称"
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
viewModel.refreshLocalDisplayState()
|
||||
|
||||
XCTAssertEqual(viewModel.currentScenicName, "")
|
||||
XCTAssertEqual(appStore.currentScenicName, "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,4 +166,69 @@ final class OrderListViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(list[1].photoTravel?.orderPhotoNum, 6)
|
||||
XCTAssertEqual(list[1].photoTravel?.needCheckIn, true)
|
||||
}
|
||||
|
||||
func testReferralOrderEntityDecodesImages() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": 1,
|
||||
"referral_order_no": "LD001",
|
||||
"order_no": "LD001",
|
||||
"user_mobile": "13800138000",
|
||||
"remark": "测试备注",
|
||||
"created_at": "2026-06-25",
|
||||
"status": 1,
|
||||
"status_label": "待处理",
|
||||
"images": ["https://cdn.example.com/a.jpg", "https://cdn.example.com/b.jpg"]
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity = try JSONDecoder().decode(ReferralOrderEntity.self, from: json)
|
||||
XCTAssertEqual(entity.images.count, 2)
|
||||
XCTAssertEqual(OrderSourceMapping.lead(from: entity).remark, "测试备注")
|
||||
}
|
||||
|
||||
func testOrderSourceLeadMappingUsesStatusLabelFallback() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": 1,
|
||||
"referral_order_no": "LD002",
|
||||
"order_no": "LD002",
|
||||
"user_mobile": "13800138000",
|
||||
"remark": "",
|
||||
"created_at": "",
|
||||
"status": 1,
|
||||
"status_label": "已完成",
|
||||
"images": []
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let entity = try JSONDecoder().decode(ReferralOrderEntity.self, from: json)
|
||||
XCTAssertEqual(OrderSourceMapping.lead(from: entity).remark, "已完成")
|
||||
}
|
||||
|
||||
func testOrderSourceSelectionFromBoundReferralUsesRemarkFallback() throws {
|
||||
let json = """
|
||||
{
|
||||
"id": 53,
|
||||
"order_no": "LD202606250008",
|
||||
"sale_user_id": 6,
|
||||
"saler_name": "管铜彬",
|
||||
"photog_remark_name": "管铜彬1234567",
|
||||
"saler_phone": "19281871413",
|
||||
"display_text": "管铜彬1234567 19281871413",
|
||||
"user_mobile": "13333333331",
|
||||
"remark": "",
|
||||
"bind_order_number": "260625128009",
|
||||
"source_type": 2,
|
||||
"source_type_label": "下级线索",
|
||||
"status": 2,
|
||||
"status_label": "已完成",
|
||||
"created_at": "2026-06-25 17:13:51"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let referral = try JSONDecoder().decode(BoundReferralOrderEntity.self, from: json)
|
||||
let selection = OrderSourceMapping.selection(from: referral)
|
||||
XCTAssertEqual(selection?.person.name, "管铜彬1234567")
|
||||
XCTAssertEqual(selection?.lead.remark, "管铜彬1234567 19281871413")
|
||||
XCTAssertEqual(selection?.lead.key, "LD202606250008")
|
||||
}
|
||||
}
|
||||
|
||||
56
suixinkanTests/PermissionApplyStatusViewModelTests.swift
Normal file
56
suixinkanTests/PermissionApplyStatusViewModelTests.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// PermissionApplyStatusViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 权限申请审核状态 ViewModel 测试。
|
||||
final class PermissionApplyStatusViewModelTests: XCTestCase {
|
||||
|
||||
func testLoadPendingDataMatchesApplyCode() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP001","role_id":1,"role_name":"摄影师","scenic_list":[{"id":1,"name":"景区A"}],"status":1,"status_label":"审核中","created_at":"2026-01-01","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [json])))
|
||||
let viewModel = PermissionApplyStatusViewModel(applyCode: "AP001")
|
||||
|
||||
await viewModel.loadPendingData(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.pendingData?.code, "AP001")
|
||||
XCTAssertEqual(viewModel.pendingData?.status, 1)
|
||||
XCTAssertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
func testNavigateToEditWhenRejected() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP002","role_id":2,"role_name":"管理员","scenic_list":[],"status":3,"status_label":"已驳回","created_at":"","audit_note":"资料不全","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [json])))
|
||||
let viewModel = PermissionApplyStatusViewModel(applyCode: "AP002")
|
||||
await viewModel.loadPendingData(api: api)
|
||||
|
||||
var edited: RoleApplyPendingResponse?
|
||||
viewModel.onNavigateToEdit = { edited = $0 }
|
||||
viewModel.navigateToEditIfRejected()
|
||||
|
||||
XCTAssertEqual(edited?.status, 3)
|
||||
}
|
||||
|
||||
func testNavigateToEditIgnoredWhenReviewing() async throws {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP003","role_id":1,"role_name":"摄影师","scenic_list":[],"status":1,"status_label":"审核中","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [json])))
|
||||
let viewModel = PermissionApplyStatusViewModel(applyCode: "AP003")
|
||||
await viewModel.loadPendingData(api: api)
|
||||
|
||||
var edited: RoleApplyPendingResponse?
|
||||
viewModel.onNavigateToEdit = { edited = $0 }
|
||||
viewModel.navigateToEditIfRejected()
|
||||
|
||||
XCTAssertNil(edited)
|
||||
}
|
||||
}
|
||||
85
suixinkanTests/PermissionApplyViewModelTests.swift
Normal file
85
suixinkanTests/PermissionApplyViewModelTests.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// PermissionApplyViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 权限申请 ViewModel 测试。
|
||||
final class PermissionApplyViewModelTests: XCTestCase {
|
||||
|
||||
private var appStore: AppStore!
|
||||
private var defaults: UserDefaults!
|
||||
|
||||
override func setUp() {
|
||||
defaults = UserDefaults(suiteName: "PermissionApplyViewModelTests")!
|
||||
defaults.removePersistentDomain(forName: "PermissionApplyViewModelTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
appStore.saveRolePermissionList([
|
||||
RolePermissionResponse(
|
||||
role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"),
|
||||
scenic: [ScenicInfo(id: 100, name: "已有景区", status: 1)]
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
appStore.logout()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testExistingScenicIsDisabledAfterLoad() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":2,"list":[{"id":100,"name":"已有景区"},{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "拍摄", isSelected: true))
|
||||
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
|
||||
let existing = viewModel.availableScenics.first { $0.id == 100 }
|
||||
let fresh = viewModel.availableScenics.first { $0.id == 101 }
|
||||
XCTAssertEqual(existing?.isDisabled, true)
|
||||
XCTAssertEqual(existing?.isSelected, true)
|
||||
XCTAssertEqual(fresh?.isDisabled, false)
|
||||
}
|
||||
|
||||
func testSubmitValidationRequiresRoleAndScenic() async {
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.submit(api: api)
|
||||
XCTAssertEqual(message, "请先选择角色")
|
||||
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.submit(api: api)
|
||||
XCTAssertEqual(message, "请至少选择一个景区")
|
||||
}
|
||||
|
||||
func testSubmitSuccessReturnsApplyCode() async throws {
|
||||
let scenicJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}}
|
||||
""".data(using: .utf8)!
|
||||
let submitJSON = """
|
||||
{"code":100000,"msg":"success","data":{"code":"AP999"}}
|
||||
""".data(using: .utf8)!
|
||||
let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON, submitJSON])))
|
||||
let viewModel = PermissionApplyViewModel(appStore: appStore)
|
||||
viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true))
|
||||
await viewModel.loadAvailableScenics(api: api)
|
||||
if let scenic = viewModel.availableScenics.first(where: { $0.id == 101 }) {
|
||||
viewModel.toggleScenic(scenic)
|
||||
}
|
||||
|
||||
var applyCode: String?
|
||||
viewModel.onSubmitSuccess = { applyCode = $0 }
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertEqual(applyCode, "AP999")
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,22 @@ final class ProfileViewModelTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testAccountTypeLabelForStoreAccount() {
|
||||
AppStore.shared.accountType = V9StoreUser.accountTypeValue
|
||||
let viewModel = ProfileViewModel()
|
||||
XCTAssertEqual(viewModel.accountTypeLabel, "门店账号")
|
||||
XCTAssertTrue(viewModel.isStoreAccount)
|
||||
AppStore.shared.accountType = ""
|
||||
}
|
||||
|
||||
func testAccountTypeLabelForScenicAccount() {
|
||||
AppStore.shared.accountType = V9ScenicUser.accountTypeValue
|
||||
let viewModel = ProfileViewModel()
|
||||
XCTAssertEqual(viewModel.accountTypeLabel, "景区账号")
|
||||
XCTAssertFalse(viewModel.isStoreAccount)
|
||||
AppStore.shared.accountType = ""
|
||||
}
|
||||
|
||||
func testApplyLocalProfileUpdateSyncsNicknameAndAvatar() {
|
||||
AppStore.shared.userName = "全局昵称"
|
||||
AppStore.shared.avatar = "https://cdn.example.com/new.jpg"
|
||||
|
||||
28
suixinkanTests/ScenicDistanceCalculatorTests.swift
Normal file
28
suixinkanTests/ScenicDistanceCalculatorTests.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// ScenicDistanceCalculatorTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 景区距离计算测试。
|
||||
final class ScenicDistanceCalculatorTests: XCTestCase {
|
||||
|
||||
func testDistanceKilometersBetweenKnownPoints() {
|
||||
// 北京天安门 ~ 故宫,约 0.9km
|
||||
let km = ScenicDistanceCalculator.distanceKilometers(
|
||||
fromLat: 39.9087,
|
||||
fromLng: 116.3975,
|
||||
toLat: 39.9163,
|
||||
toLng: 116.3972
|
||||
)
|
||||
XCTAssertGreaterThan(km, 0.5)
|
||||
XCTAssertLessThan(km, 1.5)
|
||||
}
|
||||
|
||||
func testFormatDistanceKilometers() {
|
||||
XCTAssertEqual(ScenicDistanceCalculator.formatDistanceKilometers(1.234), "1.23")
|
||||
XCTAssertEqual(ScenicDistanceCalculator.formatDistanceKilometers(0), "0")
|
||||
}
|
||||
}
|
||||
102
suixinkanTests/ScenicSelectionViewModelTests.swift
Normal file
102
suixinkanTests/ScenicSelectionViewModelTests.swift
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// ScenicSelectionViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 景区选择 ViewModel 测试。
|
||||
final class ScenicSelectionViewModelTests: XCTestCase {
|
||||
|
||||
private var appStore: AppStore!
|
||||
private var defaults: UserDefaults!
|
||||
|
||||
override func setUp() {
|
||||
defaults = UserDefaults(suiteName: "ScenicSelectionViewModelTests")!
|
||||
defaults.removePersistentDomain(forName: "ScenicSelectionViewModelTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
appStore.logout()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSearchFiltersByNameAndAddress() {
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 1, name: "西湖景区", status: 1, location: ScenicLocationInfo(lng: 120.1, lat: 30.2, address: "杭州西湖"), coverImg: nil),
|
||||
ScenicInfo(id: 2, name: "灵隐寺", status: 1, location: ScenicLocationInfo(lng: 120.2, lat: 30.3, address: "杭州灵隐"), coverImg: nil),
|
||||
])
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore)
|
||||
|
||||
viewModel.loadFromLocal()
|
||||
XCTAssertEqual(viewModel.spots.count, 2)
|
||||
|
||||
viewModel.onSearchQueryChange("西湖")
|
||||
XCTAssertEqual(viewModel.spots.count, 1)
|
||||
XCTAssertEqual(viewModel.spots.first?.name, "西湖景区")
|
||||
|
||||
viewModel.onSearchQueryChange("灵隐")
|
||||
XCTAssertEqual(viewModel.spots.count, 1)
|
||||
XCTAssertEqual(viewModel.spots.first?.name, "灵隐寺")
|
||||
}
|
||||
|
||||
func testNearestTagWhenLocationProvided() {
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 1, name: "近", status: 1, location: ScenicLocationInfo(lng: 116.40, lat: 39.91, address: "A"), coverImg: nil),
|
||||
ScenicInfo(id: 2, name: "远", status: 1, location: ScenicLocationInfo(lng: 116.50, lat: 39.95, address: "B"), coverImg: nil),
|
||||
])
|
||||
let items = ScenicSpotDisplayMapper.buildItems(
|
||||
scenicList: appStore.roleScenicList(),
|
||||
searchQuery: "",
|
||||
currentLat: 39.9087,
|
||||
currentLng: 116.3975,
|
||||
selectedScenicId: 0
|
||||
)
|
||||
XCTAssertEqual(items.first?.openStatus, .nearest)
|
||||
}
|
||||
|
||||
func testClosedStatusTag() {
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 1, name: "停业", status: 2, location: nil, coverImg: nil),
|
||||
])
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore)
|
||||
viewModel.loadFromLocal()
|
||||
XCTAssertEqual(viewModel.spots.first?.openStatus, .closed)
|
||||
XCTAssertEqual(viewModel.spots.first?.statusLabel, "暂未营业")
|
||||
}
|
||||
|
||||
func testSelectSpotWritesAppStore() {
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 5, name: "测试景区", status: 1),
|
||||
])
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore)
|
||||
var completed = false
|
||||
viewModel.onSelectionComplete = { completed = true }
|
||||
|
||||
viewModel.loadFromLocal()
|
||||
viewModel.selectSpot(viewModel.spots[0])
|
||||
|
||||
XCTAssertEqual(appStore.currentScenicId, 5)
|
||||
XCTAssertEqual(appStore.currentScenicName, "测试景区")
|
||||
XCTAssertTrue(completed)
|
||||
XCTAssertTrue(viewModel.spots.first?.isSelected == true)
|
||||
}
|
||||
|
||||
func testOnApplyNowRoutesToStatusWhenPendingReviewing() async throws {
|
||||
let pendingJSON = """
|
||||
{"code":100000,"msg":"success","data":[{"code":"AP001","role_id":1,"role_name":"摄影师","scenic_list":[],"status":1,"status_label":"审核中","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
||||
""".data(using: .utf8)!
|
||||
let api = await MainActor.run {
|
||||
HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [pendingJSON])))
|
||||
}
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore)
|
||||
var statusCode: String?
|
||||
viewModel.onNavigateApplyStatus = { statusCode = $0 }
|
||||
|
||||
await viewModel.onApplyNow(api: api)
|
||||
|
||||
XCTAssertEqual(statusCode, "AP001")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user