Files
suixinkan_ios_new/suixinkanTests/PaymentViewModelTests.swift
汉秋 3dcfb99254 对齐 Android 立即收款页面 UI,并补齐推送状态与语音开关。
统一收款详情布局、设置金额弹窗和收款记录样式,接入 type 6/1 推送驱动与轮询 fallback,使 iOS 收款反馈与 Android 一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 15:17:09 +08:00

183 lines
7.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// PaymentViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class PaymentViewModelTests: XCTestCase {
///
func testLoadPayCodeWithoutScenicClearsState() async {
let api = PaymentMockAPI()
let viewModel = makeViewModel()
viewModel.staticPayUrl = "old"
await viewModel.loadPayCode(api: api, scenicId: nil)
XCTAssertTrue(viewModel.staticPayUrl.isEmpty)
XCTAssertEqual(api.payCodeScenicIds, [])
XCTAssertEqual(viewModel.errorMessage, "请先选择景区")
}
/// URL
func testLoadPayCodeUsesScenicIdAndAppliesStaticURL() async {
let api = PaymentMockAPI()
api.payCodeResponse = PayCodeResponse(staticPayUrl: "https://pay.example.com/static", dynamicPayUrl: "https://pay.example.com/dynamic")
let viewModel = makeViewModel()
await viewModel.loadPayCode(api: api, scenicId: 9)
XCTAssertEqual(api.payCodeScenicIds, [9])
XCTAssertEqual(viewModel.currentPayUrl, "https://pay.example.com/static")
XCTAssertTrue(viewModel.hasStaticPayCode)
}
/// URL
func testApplyDynamicAmountBuildsDynamicURL() async {
let viewModel = makeViewModel()
viewModel.dynamicPayUrl = "https://pay.example.com/dynamic?scenic_id=9"
viewModel.amountText = "12.345"
viewModel.remarkText = "茶水"
XCTAssertTrue(viewModel.applyDynamicAmount())
XCTAssertTrue(viewModel.currentPayUrl.contains("amount=12.35"))
XCTAssertTrue(viewModel.currentPayUrl.contains("remark="))
XCTAssertEqual(viewModel.status, .waiting)
}
///
func testConfirmAmountUpdatesQRCode() {
let viewModel = makeViewModel()
viewModel.staticPayUrl = "https://pay.example.com/static"
viewModel.dynamicPayUrl = "https://pay.example.com/dynamic?scenic_id=9"
viewModel.amountText = "18.50"
viewModel.remarkText = "门票"
XCTAssertTrue(viewModel.confirmAmount())
XCTAssertTrue(viewModel.currentPayUrl.contains("amount=18.5"))
XCTAssertEqual(viewModel.status, .waiting)
XCTAssertNil(viewModel.scanSuccessTime)
XCTAssertNil(viewModel.paymentSuccessData)
}
///
func testHandlePaymentPushScanSuccess() {
let viewModel = makeViewModel()
viewModel.paymentSuccessData = PaymentSuccessDisplayData(
orderAmount: "10",
orderNumber: "A",
payTime: "2026-06-22 10:00",
remark: ""
)
viewModel.handlePaymentPush(type: 6, dataJSON: nil)
XCTAssertNotNil(viewModel.scanSuccessTime)
XCTAssertNil(viewModel.paymentSuccessData)
}
///
func testHandlePaymentPushPaymentSuccess() {
let viewModel = makeViewModel()
viewModel.scanSuccessTime = "2026-06-22 10:00"
let payload = """
{"order_amount":"88.00","order_number":"ORDER-1","pay_time":"2026-06-22 10:01","remark":""}
"""
viewModel.handlePaymentPush(type: 1, dataJSON: payload)
XCTAssertNil(viewModel.scanSuccessTime)
XCTAssertEqual(viewModel.paymentSuccessData?.orderAmount, "88.00")
XCTAssertEqual(viewModel.paymentSuccessData?.orderNumber, "ORDER-1")
XCTAssertEqual(viewModel.paymentSuccessData?.payTime, "2026-06-22 10:01")
XCTAssertEqual(viewModel.paymentSuccessData?.remark, "测试")
}
///
func testToggleVoiceBroadcastPersistsPreference() {
let defaults = UserDefaults(suiteName: "PaymentViewModelTests.voice")!
defaults.removePersistentDomain(forName: "PaymentViewModelTests.voice")
let preferences = AppPreferencesStore(defaults: defaults)
let viewModel = makeViewModel(preferencesStore: preferences)
XCTAssertFalse(viewModel.isVoiceBroadcast)
viewModel.toggleVoiceBroadcast()
XCTAssertTrue(viewModel.isVoiceBroadcast)
XCTAssertTrue(preferences.loadPaymentReceiveVoiceEnabled())
let reloaded = makeViewModel(preferencesStore: preferences)
XCTAssertTrue(reloaded.isVoiceBroadcast)
}
///
func testPollingFindsNewPaymentRecord() async {
let api = PaymentMockAPI()
api.recordResponses = [
PaymentCollectionRecordResponse(list: [
PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-22", createTime: "10:00")
]),
PaymentCollectionRecordResponse(list: [
PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-22", createTime: "10:00"),
PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.35", createDate: "2026-06-22", createTime: "10:01")
])
]
let viewModel = makeViewModel()
viewModel.amountText = "12.35"
viewModel.remarkText = "备注"
await viewModel.primePaymentRecords(api: api, scenicId: 9)
await viewModel.pollUntilPaymentDetected(api: api, scenicId: 9, maxAttempts: 1, intervalNanoseconds: 0)
XCTAssertEqual(viewModel.status, .success(PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.35", createDate: "2026-06-22", createTime: "10:01")))
XCTAssertEqual(viewModel.paymentSuccessData?.orderAmount, "12.35")
XCTAssertEqual(viewModel.paymentSuccessData?.orderNumber, "B")
XCTAssertEqual(viewModel.paymentSuccessData?.payTime, "2026-06-22 10:01")
XCTAssertEqual(viewModel.paymentSuccessData?.remark, "备注")
}
///
func testRecordGroupsFallbackFromList() {
let response = PaymentCollectionRecordResponse(list: [
PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-21", createTime: "10:00"),
PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.00", createDate: "2026-06-21", createTime: "10:01")
])
let groups = PaymentCollectionRecordViewModel.makeGroups(from: response)
XCTAssertEqual(groups.count, 1)
XCTAssertEqual(groups[0].analyse.orderCount, 2)
XCTAssertEqual(groups[0].analyse.orderAmountSum, "20")
}
/// ViewModel
private func makeViewModel(preferencesStore: AppPreferencesStore = AppPreferencesStore(defaults: UserDefaults(suiteName: "PaymentViewModelTests.default")!)) -> PaymentCollectionViewModel {
PaymentCollectionViewModel(preferencesStore: preferencesStore)
}
}
@MainActor
/// API
private final class PaymentMockAPI: PaymentServing {
var payCodeResponse = PayCodeResponse()
var recordResponses: [PaymentCollectionRecordResponse] = []
private(set) var payCodeScenicIds: [Int] = []
private(set) var recordScenicIds: [Int] = []
/// ID
func payCode(scenicId: Int) async throws -> PayCodeResponse {
payCodeScenicIds.append(scenicId)
return payCodeResponse
}
/// ID
func paymentCollectionRecords(scenicId: Int) async throws -> PaymentCollectionRecordResponse {
recordScenicIds.append(scenicId)
return recordResponses.isEmpty ? PaymentCollectionRecordResponse() : recordResponses.removeFirst()
}
}