// // PaymentCollectionDetailsViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan /// 收款详情 ViewModel 测试。 @MainActor final class PaymentCollectionDetailsViewModelTests: XCTestCase { private var appStore: AppStore! override func setUp() { let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests")! defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests") appStore = AppStore(defaults: defaults) appStore.session.configureTestAccount(userId: "10001", scenicId: 10) appStore.session.currentScenicName = "测试景区" } override func tearDown() { appStore.logout() super.tearDown() } func testPaymentAmountValidatorRejectsEmptyAmount() { XCTAssertEqual(PaymentAmountValidator.validationMessage(for: ""), "请输入收款金额") } func testPaymentAmountValidatorRejectsZeroAmount() { XCTAssertEqual(PaymentAmountValidator.validationMessage(for: "0"), "请输入有效的收款金额") } func testPaymentAmountValidatorRejectsMoreThanTwoDecimals() { XCTAssertEqual(PaymentAmountValidator.validationMessage(for: "1.234"), "金额最多支持两位小数") } func testPaymentAmountValidatorAcceptsValidAmount() { XCTAssertNil(PaymentAmountValidator.validationMessage(for: "12.50")) } func testShowSetAmountDialogRequiresPayCode() { let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) var message: String? viewModel.onShowMessage = { message = $0 } viewModel.showSetAmountDialog() XCTAssertEqual(message, "请先获取收款码") XCTAssertFalse(viewModel.showAmountDialog) } func testConfirmAmountBuildsDynamicURL() async throws { let payCodeJSON = """ {"code":100000,"msg":"success","data":{"static_pay_url":"https://pay.example.com/static","dynamic_pay_url":"https://pay.example.com/dynamic"}} """.data(using: .utf8)! let session = MockURLSession(responses: [payCodeJSON]) let api = PaymentAPI(client: APIClient(environment: .testing, session: session)) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) await viewModel.loadPayCode(api: api) viewModel.updateAmount("88.50") viewModel.updateRemark("测试备注") viewModel.confirmAmount() XCTAssertTrue(viewModel.hasPayCode) XCTAssertFalse(viewModel.showAmountDialog) } func testToggleReceiveVoicePersistsInAppStore() { let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) viewModel.refreshLocalState() XCTAssertFalse(viewModel.isVoiceBroadcastOpen) viewModel.toggleReceiveVoice() XCTAssertTrue(appStore.payment.isOpenReceiveVoice) XCTAssertTrue(viewModel.isVoiceBroadcastOpen) } func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() { appStore.session.userName = "摄影师小王" appStore.permissions.saveRolePermissionList([ RolePermissionResponse( role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"), store: [ RolePermissionStoreInfo(id: 1, name: "空中草原店"), RolePermissionStoreInfo(id: 2, name: "河谷草原店"), ] ), ]) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) XCTAssertEqual(viewModel.displayPhotographerName, "摄影师小王") XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店") } func testLoadPayCodeClearsQRWhenScenicMissing() async { appStore.session.currentScenicId = 0 let session = MockURLSession(responses: []) let api = PaymentAPI(client: APIClient(environment: .testing, session: session)) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) await viewModel.loadPayCode(api: api) XCTAssertFalse(viewModel.hasPayCode) XCTAssertEqual(session.requests.count, 0) } func testNalatiLoadsCachedConfigBeforeRefreshingRemoteConfig() async throws { let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests.Config")! defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests.Config") let cache = PayPageConfigCache(defaults: defaults, environment: .testing) let cached = PayPageConfig( background: "https://cache.example.com/background.png", logo: "https://cache.example.com/logo.png", title: "缓存标题", subtitle: "缓存副标题" ) cache.save(cached, scenicId: 128) appStore.session.currentScenicId = 128 appStore.session.currentScenicName = "那拉提景区" let responseJSON = """ {"code":100000,"msg":"success","data":{ "background":"https://new.example.com/background.png", "logo":"", "title":"最新标题", "subtitle":"" }} """.data(using: .utf8)! let session = MockURLSession(responses: [responseJSON]) let api = PaymentAPI(client: APIClient(environment: .testing, session: session)) let viewModel = PaymentCollectionDetailsViewModel( appStore: appStore, payPageConfigCache: cache ) XCTAssertEqual(viewModel.payPageConfig, cached) await viewModel.loadPayPageConfig(api: api) XCTAssertEqual(viewModel.payPageConfig.background, "https://new.example.com/background.png") XCTAssertEqual(viewModel.payPageConfig.logo, cached.logo) XCTAssertEqual(viewModel.payPageConfig.title, "最新标题") XCTAssertEqual(viewModel.payPageConfig.subtitle, cached.subtitle) XCTAssertEqual(viewModel.brandingRefreshVersion, 1) XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/pay-page-config") XCTAssertEqual( URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)? .queryItems? .first(where: { $0.name == "scenic_id" })? .value, "128" ) } func testNonNalatiDoesNotRequestPayPageConfig() async { let session = MockURLSession(responses: []) let api = PaymentAPI(client: APIClient(environment: .testing, session: session)) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) await viewModel.loadPayPageConfig(api: api) XCTAssertFalse(viewModel.usesNalatiBranding) XCTAssertEqual(session.requests.count, 0) } func testPayPageConfigCacheIsEnvironmentScoped() { let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests.Environment")! defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests.Environment") let production = PayPageConfigCache(defaults: defaults, environment: .production) let testing = PayPageConfigCache(defaults: defaults, environment: .testing) production.save(.nalatiDefault, scenicId: 128) XCTAssertEqual(production.config(scenicId: 128), .nalatiDefault) XCTAssertNil(testing.config(scenicId: 128)) } }