Files
suixinkan_uikit/suixinkanTests/PaymentCollectionDetailsViewModelTests.swift

306 lines
14 KiB
Swift

//
// PaymentCollectionDetailsViewModelTests.swift
// suixinkanTests
//
import XCTest
import UIKit
@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 testPayCodeLoadingStateCoversInitialRenderAndRequest() async {
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)
var loadingStates = [viewModel.isPayCodeLoading]
viewModel.onStateChange = {
loadingStates.append(viewModel.isPayCodeLoading)
}
await viewModel.loadPayCode(api: api)
XCTAssertEqual(loadingStates.first, true)
XCTAssertTrue(loadingStates.dropLast().allSatisfy { $0 })
XCTAssertEqual(loadingStates.last, false)
XCTAssertTrue(viewModel.hasPayCode)
}
func testToggleReceiveVoicePersistsInAppStore() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
viewModel.refreshLocalState()
XCTAssertFalse(viewModel.isVoiceBroadcastOpen)
viewModel.toggleReceiveVoice()
XCTAssertTrue(appStore.payment.isOpenReceiveVoice)
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
}
func testNalatiAccountInfoUsesPhotographerRealNameAndAllPermissionStores() {
appStore.session.userName = "摄影师昵称"
appStore.session.realName = "摄影师真实姓名"
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 testPhotographerNameDoesNotFallBackToNicknameWhenRealNameMissing() {
appStore.session.userName = "摄影师昵称"
appStore.session.realName = " "
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
XCTAssertEqual(viewModel.displayPhotographerName, "-")
}
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.isPayCodeLoading)
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 testUnbrandedScenicDoesNotRequestPayPageConfig() 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.usesBranding)
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))
}
func testSailimuRefreshPersistsOnlyItsOwnConfigAndReusesBrandUI() async throws {
let suite = "PaymentCollectionDetailsViewModelTests.Sailimu.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let cache = PayPageConfigCache(defaults: defaults, environment: .testing)
cache.save(.nalatiDefault, scenicId: 128)
let cached = PayPageConfig(
background: "https://cache.example.com/sailimu.png",
logo: "https://cache.example.com/sailimu-logo.png",
title: "缓存赛里木湖标题",
subtitle: "赛里木湖景区"
)
cache.save(cached, scenicId: 125)
appStore.session.currentScenicId = 125
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
XCTAssertTrue(viewModel.usesBranding)
XCTAssertEqual(viewModel.payPageConfig, cached)
let response = """
{"code":100000,"msg":"success","data":{
"background":"https://new.example.com/sailimu.png",
"logo":"", "title":"最新赛里木湖标题", "subtitle":""
}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [response])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
await viewModel.loadPayPageConfig(api: api)
let url = try XCTUnwrap(session.requests.first?.url)
XCTAssertEqual(url.path, "/api/yf-handset-app/photog/pay-page-config")
XCTAssertEqual(URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "scenic_id" })?.value, "125")
XCTAssertEqual(viewModel.payPageConfig.title, "最新赛里木湖标题")
XCTAssertEqual(viewModel.payPageConfig.background, "https://new.example.com/sailimu.png")
XCTAssertEqual(viewModel.payPageConfig.logo, cached.logo)
XCTAssertEqual(viewModel.payPageConfig.subtitle, cached.subtitle)
XCTAssertEqual(viewModel.brandingRefreshVersion, 1)
XCTAssertEqual(cache.config(scenicId: 128), .nalatiDefault)
XCTAssertEqual(cache.config(scenicId: 125), viewModel.payPageConfig)
let reopened = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
XCTAssertEqual(reopened.payPageConfig, viewModel.payPageConfig)
appStore.session.currentScenicId = 128
viewModel.refreshLocalState()
XCTAssertEqual(viewModel.payPageConfig, .nalatiDefault)
appStore.session.currentScenicId = 125
viewModel.refreshLocalState()
XCTAssertEqual(viewModel.payPageConfig, reopened.payPageConfig)
}
func testSailimuRequestFailurePreservesDefaultsOrItsOwnCache() async {
let suite = "PaymentCollectionDetailsViewModelTests.SailimuFailure.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let cache = PayPageConfigCache(defaults: defaults, environment: .testing)
cache.save(.nalatiDefault, scenicId: 128)
appStore.session.currentScenicId = 125
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore, payPageConfigCache: cache)
let session = MockURLSession(responses: [])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
XCTAssertEqual(viewModel.payPageConfig, .sailimuDefault)
await viewModel.loadPayPageConfig(api: api)
XCTAssertEqual(viewModel.payPageConfig, .sailimuDefault)
XCTAssertNil(cache.config(scenicId: 125))
let cached = PayPageConfig(background: "https://cache.example.com/lake.png",
logo: "", title: "缓存标题", subtitle: "赛里木湖")
cache.save(cached, scenicId: 125)
viewModel.refreshLocalState()
await viewModel.loadPayPageConfig(api: api)
XCTAssertEqual(viewModel.payPageConfig, cached)
XCTAssertEqual(cache.config(scenicId: 125), cached)
XCTAssertEqual(cache.config(scenicId: 128), .nalatiDefault)
XCTAssertEqual(viewModel.brandingRefreshVersion, 0)
}
func testBrandPresentationUsesMatchingBundledAssetsAndTitleLines() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
for (scenicId, prefix, lines) in [(125, "payment_sailimu", 2), (128, "payment_nalati", 1)] {
appStore.session.currentScenicId = scenicId
viewModel.refreshLocalState()
XCTAssertTrue(viewModel.usesBranding)
XCTAssertEqual(viewModel.brandBackgroundAssetName, "\(prefix)_background")
XCTAssertEqual(viewModel.brandLogoAssetName, "\(prefix)_logo")
XCTAssertEqual(viewModel.brandTitleNumberOfLines, lines)
XCTAssertNotNil(UIImage(named: viewModel.brandBackgroundAssetName))
XCTAssertNotNil(UIImage(named: viewModel.brandLogoAssetName))
}
}
}