完善实名认证、钱包与打卡点模块 UI 与业务逻辑。
对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import UIKit
|
||||
@testable import suixinkan
|
||||
|
||||
/// 钱包功能模型、API 和 ViewModel 测试。
|
||||
@ -71,6 +72,8 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[1])["page"], "1")
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[2])["start_date"], "2026-07-01")
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[6])["staff_id"], "9")
|
||||
XCTAssertNil(session.requests[4].httpBody)
|
||||
XCTAssertNil(session.requests[6].httpBody)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[5]).contains(#""sms_code":"123456""#), true)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[7]).contains(#""points":24"#), true)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[8]).contains(#""page_size":10"#), true)
|
||||
@ -96,6 +99,46 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertGreaterThanOrEqual(api.earningQueries.count, 2)
|
||||
}
|
||||
|
||||
func testWalletFiltersUseAndroidDateOffsets() async {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
let now = calendar.date(from: DateComponents(year: 2026, month: 7, day: 13, hour: 12))!
|
||||
let api = FakeWalletPageService()
|
||||
let viewModel = WalletViewModel(
|
||||
staffIdProvider: { 9 },
|
||||
calendar: calendar,
|
||||
nowProvider: { now }
|
||||
)
|
||||
|
||||
await viewModel.refreshTransaction(api: api)
|
||||
await viewModel.selectFilter(.last30Days, api: api)
|
||||
await viewModel.selectFilter(.last180Days, api: api)
|
||||
|
||||
XCTAssertEqual(api.earningQueries.map(\.start), ["2026-07-06", "2026-06-13", "2026-01-14"])
|
||||
XCTAssertEqual(api.earningQueries.map(\.end), ["2026-07-13", "2026-07-13", "2026-07-13"])
|
||||
}
|
||||
|
||||
func testRefreshFailureKeepsWalletAndPointRecords() async {
|
||||
let api = FakeWalletPageService()
|
||||
let walletViewModel = WalletViewModel(staffIdProvider: { 9 })
|
||||
let pointsViewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||
|
||||
await walletViewModel.refreshWithdraw(api: api)
|
||||
await walletViewModel.refreshTransaction(api: api)
|
||||
await pointsViewModel.refreshWithdrawList(api: api)
|
||||
api.shouldFailWithdrawList = true
|
||||
api.shouldFailEarningDetail = true
|
||||
api.shouldFailPointWithdrawList = true
|
||||
|
||||
await walletViewModel.refreshWithdraw(api: api)
|
||||
await walletViewModel.refreshTransaction(api: api)
|
||||
await pointsViewModel.refreshWithdrawList(api: api)
|
||||
|
||||
XCTAssertEqual(walletViewModel.withdrawRecords.count, 1)
|
||||
XCTAssertEqual(walletViewModel.transactionGroups.count, 1)
|
||||
XCTAssertEqual(pointsViewModel.withdrawRecords.count, 1)
|
||||
}
|
||||
|
||||
func testWalletWithdrawDestinationFlow() async throws {
|
||||
let viewModel = WalletViewModel()
|
||||
|
||||
@ -107,6 +150,12 @@ final class WalletFeatureTests: XCTestCase {
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
XCTAssertEqual(destination, .realNamePending)
|
||||
|
||||
profile = FakeWalletProfileService(realNameStatus: 3, bankCardStatus: nil)
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
guard case .realNameAudit = destination else {
|
||||
return XCTFail("实名认证驳回应进入审核页")
|
||||
}
|
||||
|
||||
profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: nil)
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
XCTAssertEqual(destination, .withdrawalSettings)
|
||||
@ -115,6 +164,12 @@ final class WalletFeatureTests: XCTestCase {
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
XCTAssertEqual(destination, .bankCardPending)
|
||||
|
||||
profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: 3)
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
guard case .withdrawalAudit = destination else {
|
||||
return XCTFail("银行卡驳回应进入审核页")
|
||||
}
|
||||
|
||||
profile = FakeWalletProfileService(realNameStatus: 2, bankCardStatus: 2)
|
||||
destination = try await viewModel.resolveWithdrawDestination(profileAPI: profile)
|
||||
XCTAssertEqual(destination, .withdraw)
|
||||
@ -129,11 +184,21 @@ final class WalletFeatureTests: XCTestCase {
|
||||
viewModel.updateVerificationCode("12a456")
|
||||
|
||||
XCTAssertEqual(viewModel.amount, "12.34")
|
||||
XCTAssertEqual(viewModel.verificationCode, "12456")
|
||||
XCTAssertEqual(viewModel.verificationCode, "12a456")
|
||||
|
||||
let success = await viewModel.submit(api: api)
|
||||
XCTAssertTrue(success)
|
||||
XCTAssertEqual(api.withdrawApplyRequests.first?.amount, "12.34")
|
||||
|
||||
viewModel.updateAmount("0")
|
||||
XCTAssertNil(viewModel.validationMessage)
|
||||
viewModel.updateAmount("99")
|
||||
XCTAssertEqual(viewModel.validationMessage, "金额不能大于可提现金额")
|
||||
|
||||
await viewModel.requestVerificationCode(api: api)
|
||||
XCTAssertEqual(viewModel.countdown, 60)
|
||||
viewModel.decrementCountdown()
|
||||
XCTAssertEqual(viewModel.countdown, 59)
|
||||
}
|
||||
|
||||
func testPointsRedemptionViewModelClampsAndRefreshes() async {
|
||||
@ -141,6 +206,8 @@ final class WalletFeatureTests: XCTestCase {
|
||||
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
viewModel.updatePointsInput("0")
|
||||
XCTAssertEqual(viewModel.pointsInput, "0")
|
||||
viewModel.updatePointsInput("999")
|
||||
XCTAssertEqual(viewModel.pointsInput, "60")
|
||||
|
||||
@ -151,6 +218,18 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
||||
}
|
||||
|
||||
func testWalletTokensAndHomeIconMatchAndroid() {
|
||||
XCTAssertEqual(WalletTokens.pageBackground.walletHexRGB, 0xF5F6F8)
|
||||
XCTAssertEqual(WalletTokens.withdrawPageBackground.walletHexRGB, 0xF9FAFB)
|
||||
XCTAssertEqual(WalletTokens.summaryBackground.walletHexRGB, 0x1677FF)
|
||||
XCTAssertEqual(WalletTokens.primary.walletHexRGB, 0x0073FF)
|
||||
XCTAssertEqual(WalletTokens.summaryRadius, 24)
|
||||
XCTAssertEqual(WalletTokens.contentRadius, 16)
|
||||
XCTAssertEqual(WalletTokens.cardRadius, 12)
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "wallet")?.iconName, "home_menu_wallet")
|
||||
XCTAssertNotNil(UIImage(named: "home_menu_wallet"))
|
||||
}
|
||||
|
||||
private static func envelope(_ dataJSON: String) -> Data {
|
||||
Data(#"{"code":100000,"msg":"success","data":\#(dataJSON)}"#.utf8)
|
||||
}
|
||||
@ -175,6 +254,9 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
|
||||
private(set) var withdrawApplyRequests: [(amount: String, smsCode: String)] = []
|
||||
private(set) var pointApplyRequests: [Int] = []
|
||||
var shouldFailWithdrawList = false
|
||||
var shouldFailEarningDetail = false
|
||||
var shouldFailPointWithdrawList = false
|
||||
|
||||
func walletSummary(type: Int) async throws -> WalletSummaryResponse {
|
||||
WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "80.00", amountWithdrawable: "50.00")
|
||||
@ -191,6 +273,7 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
}
|
||||
|
||||
func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
|
||||
if shouldFailWithdrawList { throw FakeWalletError.requestFailed }
|
||||
WalletWithdrawListResponse(
|
||||
total: 1,
|
||||
item: [WalletWithdrawItem(id: page, amount: "10.00", createdAt: "2026-07-01", settlementStatus: 40, statusLabel: "打款中")]
|
||||
@ -198,6 +281,7 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
}
|
||||
|
||||
func earningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> EarningDetailResponse {
|
||||
if shouldFailEarningDetail { throw FakeWalletError.requestFailed }
|
||||
earningQueries.append((startDate, endDate, page))
|
||||
return EarningDetailResponse(
|
||||
totalAmount: "12.30",
|
||||
@ -242,10 +326,18 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
}
|
||||
|
||||
func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
|
||||
if shouldFailPointWithdrawList { throw FakeWalletError.requestFailed }
|
||||
PointWithdrawListResponse(total: 1, list: [PointWithdrawItem(id: page, points: 24, amount: 2, status: 1, createdAt: "2026-07-01")])
|
||||
}
|
||||
}
|
||||
|
||||
/// 钱包测试替身错误。
|
||||
private enum FakeWalletError: LocalizedError {
|
||||
case requestFailed
|
||||
|
||||
var errorDescription: String? { "请求失败" }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 钱包前置资料服务测试替身。
|
||||
private struct FakeWalletProfileService: WalletProfileServing {
|
||||
@ -270,3 +362,14 @@ private struct FakeWalletProfileService: WalletProfileServing {
|
||||
return try JSONDecoder().decode(BankCardInfoResponse.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
var walletHexRGB: UInt {
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
return UInt(Int(round(red * 255)) << 16 | Int(round(green * 255)) << 8 | Int(round(blue * 255)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user