完善钱包提现状态展示与举报摄影师列表交互。
对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -37,6 +37,7 @@ final class WalletFeatureTests: XCTestCase {
|
||||
func testWalletAPIUsesExpectedRequests() async throws {
|
||||
let session = MockURLSession(responses: [
|
||||
Self.envelope(#"{"amount_total":"100.00","amount_current_balance":"80.00","amount_withdrawable":"50.00"}"#),
|
||||
Self.envelope(#"{"total":0,"list":[]}"#),
|
||||
Self.envelope(#"{"total":0,"item":[]}"#),
|
||||
Self.envelope(#"{"total_amount":"0.00","total_points":0,"total":0,"list":[]}"#),
|
||||
Self.envelope(#"{"amount_withdrawable":"50.00","min_withdraw_amount":"1","max_single_withdraw_amount":"100","max_daily_withdraw_amount":"200","user_phone":"13800138000","bank_card":{"real_name":"张三","bank_name":"招商银行","card_number":"622200001234"},"settlement":"T+1","withdraw_info":["说明"]}"#),
|
||||
@ -49,6 +50,7 @@ final class WalletFeatureTests: XCTestCase {
|
||||
let api = WalletAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
_ = try await api.walletSummary(type: 0)
|
||||
_ = try await api.walletTransactionList(startDate: "2026-07-01", endDate: "2026-07-09", page: 0, pageSize: 0, type: 2)
|
||||
_ = try await api.walletWithdrawList(page: 0, pageSize: 0)
|
||||
_ = try await api.earningDetail(startDate: "2026-07-01", endDate: "2026-07-09", page: 2, pageSize: 10)
|
||||
_ = try await api.withdrawInfo()
|
||||
@ -60,6 +62,7 @@ final class WalletFeatureTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/yf-handset-app/photog/wallet/summary",
|
||||
"/api/yf-handset-app/photog/wallet/transaction-list",
|
||||
"/api/yf-handset-app/photog/wallet/withdraw-list",
|
||||
"/api/yf-handset-app/photog/wallet/earning-detail",
|
||||
"/api/yf-handset-app/photog/wallet/withdraw-info",
|
||||
@ -69,14 +72,21 @@ final class WalletFeatureTests: XCTestCase {
|
||||
"/api/yf-handset-app/photog/point/withdraw/apply",
|
||||
"/api/yf-handset-app/photog/point/withdraw/list",
|
||||
])
|
||||
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)
|
||||
XCTAssertEqual(session.requests.map(\.httpMethod), ["GET", "GET", "GET", "GET", "GET", "POST", "POST", "POST", "POST", "POST"])
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[0])["type"], "0")
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[1]), [
|
||||
"start_date": "2026-07-01", "end_date": "2026-07-09", "page": "1", "page_size": "1", "type": "2",
|
||||
])
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[2]), ["page": "1", "page_size": "1"])
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[3]), [
|
||||
"start_date": "2026-07-01", "end_date": "2026-07-09", "page": "2", "page_size": "10",
|
||||
])
|
||||
XCTAssertEqual(Self.queryItems(from: session.requests[7])["staff_id"], "9")
|
||||
XCTAssertNil(session.requests[5].httpBody)
|
||||
XCTAssertNil(session.requests[7].httpBody)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[6]).contains(#""sms_code":"123456""#), true)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[8]).contains(#""points":24"#), true)
|
||||
XCTAssertEqual(Self.bodyString(from: session.requests[9]).contains(#""page_size":10"#), true)
|
||||
}
|
||||
|
||||
func testWalletViewModelLoadsAndPaginates() async {
|
||||
@ -139,6 +149,38 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(pointsViewModel.withdrawRecords.count, 1)
|
||||
}
|
||||
|
||||
func testCurrentTabRefreshAndPaginationThresholds() async {
|
||||
let api = FakeWalletPageService()
|
||||
api.pagedRecordTotal = 25
|
||||
let walletViewModel = WalletViewModel(staffIdProvider: { 9 })
|
||||
let pointsViewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||
|
||||
await walletViewModel.refreshSelectedTab(api: api)
|
||||
XCTAssertEqual(api.withdrawListPages, [1])
|
||||
XCTAssertTrue(api.earningQueries.isEmpty)
|
||||
|
||||
await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 7, api: api)
|
||||
XCTAssertEqual(api.withdrawListPages, [1])
|
||||
await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 8, api: api)
|
||||
XCTAssertEqual(api.withdrawListPages, [1, 2])
|
||||
|
||||
await walletViewModel.selectTab(.transaction, api: api)
|
||||
XCTAssertEqual(api.earningQueries.map(\.page), [1])
|
||||
await walletViewModel.refreshSelectedTab(api: api)
|
||||
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
|
||||
await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 8, api: api)
|
||||
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
|
||||
await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 9, api: api)
|
||||
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1, 2])
|
||||
|
||||
await pointsViewModel.refreshWithdrawList(api: api)
|
||||
XCTAssertEqual(api.pointWithdrawListPages, [1])
|
||||
await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 7, api: api)
|
||||
XCTAssertEqual(api.pointWithdrawListPages, [1])
|
||||
await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 8, api: api)
|
||||
XCTAssertEqual(api.pointWithdrawListPages, [1, 2])
|
||||
}
|
||||
|
||||
func testWalletWithdrawDestinationFlow() async throws {
|
||||
let viewModel = WalletViewModel()
|
||||
|
||||
@ -201,6 +243,22 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.countdown, 59)
|
||||
}
|
||||
|
||||
func testWithdrawAllAndSubmitFailureKeepsForm() async {
|
||||
let api = FakeWalletPageService()
|
||||
let viewModel = WithdrawViewModel()
|
||||
await viewModel.load(api: api)
|
||||
viewModel.withdrawAll()
|
||||
viewModel.updateVerificationCode("123456")
|
||||
api.shouldFailWithdrawApply = true
|
||||
|
||||
let success = await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertFalse(success)
|
||||
XCTAssertEqual(viewModel.amount, "50.00")
|
||||
XCTAssertEqual(viewModel.verificationCode, "123456")
|
||||
XCTAssertEqual(viewModel.errorMessage, "请求失败")
|
||||
}
|
||||
|
||||
func testPointsRedemptionViewModelClampsAndRefreshes() async {
|
||||
let api = FakeWalletPageService()
|
||||
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||
@ -218,6 +276,20 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
||||
}
|
||||
|
||||
func testPointApplyFailureKeepsInputAndExistingRecords() async {
|
||||
let api = FakeWalletPageService()
|
||||
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||
await viewModel.loadInitial(api: api)
|
||||
viewModel.updatePointsInput("24")
|
||||
api.shouldFailPointApply = true
|
||||
|
||||
await viewModel.applyWithdraw(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.pointsInput, "24")
|
||||
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
||||
XCTAssertEqual(viewModel.errorMessage, "请求失败")
|
||||
}
|
||||
|
||||
func testWalletTokensAndHomeIconMatchAndroid() {
|
||||
XCTAssertEqual(WalletTokens.pageBackground.walletHexRGB, 0xF5F6F8)
|
||||
XCTAssertEqual(WalletTokens.withdrawPageBackground.walletHexRGB, 0xF9FAFB)
|
||||
@ -230,6 +302,32 @@ final class WalletFeatureTests: XCTestCase {
|
||||
XCTAssertNotNil(UIImage(named: "home_menu_wallet"))
|
||||
}
|
||||
|
||||
func testWalletAndPointStatusPresentationMatchesAndroid() {
|
||||
XCTAssertEqual(
|
||||
[10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).progress },
|
||||
[10, 80, 100, 40, 100, 0]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
[10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).showsTimeline },
|
||||
[true, true, true, true, false, true]
|
||||
)
|
||||
|
||||
let pending = PointWithdrawStatusDisplay(status: 1)
|
||||
XCTAssertEqual(pending.title, "提现中")
|
||||
XCTAssertEqual(pending.progress, 40)
|
||||
XCTAssertEqual(pending.timeTitle, "预计到账时间:")
|
||||
|
||||
let success = PointWithdrawStatusDisplay(status: 2)
|
||||
XCTAssertEqual(success.title, "提现成功")
|
||||
XCTAssertEqual(success.progress, 0)
|
||||
XCTAssertEqual(success.timeTitle, "到账时间:")
|
||||
|
||||
let failed = PointWithdrawStatusDisplay(status: 3)
|
||||
XCTAssertEqual(failed.title, "提现失败")
|
||||
XCTAssertEqual(failed.progress, 100)
|
||||
XCTAssertEqual(failed.timeTitle, "审核时间:")
|
||||
}
|
||||
|
||||
private static func envelope(_ dataJSON: String) -> Data {
|
||||
Data(#"{"code":100000,"msg":"success","data":\#(dataJSON)}"#.utf8)
|
||||
}
|
||||
@ -252,11 +350,16 @@ final class WalletFeatureTests: XCTestCase {
|
||||
/// 钱包页面服务测试替身。
|
||||
private final class FakeWalletPageService: WalletPageServing {
|
||||
private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
|
||||
private(set) var withdrawListPages: [Int] = []
|
||||
private(set) var pointWithdrawListPages: [Int] = []
|
||||
private(set) var withdrawApplyRequests: [(amount: String, smsCode: String)] = []
|
||||
private(set) var pointApplyRequests: [Int] = []
|
||||
var pagedRecordTotal = 1
|
||||
var shouldFailWithdrawList = false
|
||||
var shouldFailEarningDetail = false
|
||||
var shouldFailPointWithdrawList = false
|
||||
var shouldFailWithdrawApply = false
|
||||
var shouldFailPointApply = false
|
||||
|
||||
func walletSummary(type: Int) async throws -> WalletSummaryResponse {
|
||||
WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "80.00", amountWithdrawable: "50.00")
|
||||
@ -274,25 +377,32 @@ 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: "打款中")]
|
||||
withdrawListPages.append(page)
|
||||
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||
return WalletWithdrawListResponse(
|
||||
total: pagedRecordTotal,
|
||||
item: (0 ..< count).map {
|
||||
WalletWithdrawItem(id: page * 100 + $0, amount: "10.00", createdAt: "2026-07-01", settlementStatus: 40, statusLabel: "打款中")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func earningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> EarningDetailResponse {
|
||||
if shouldFailEarningDetail { throw FakeWalletError.requestFailed }
|
||||
earningQueries.append((startDate, endDate, page))
|
||||
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||
return EarningDetailResponse(
|
||||
totalAmount: "12.30",
|
||||
totalPoints: 7,
|
||||
total: 1,
|
||||
total: pagedRecordTotal,
|
||||
list: [
|
||||
EarningDetailGroup(
|
||||
date: "2026-07-01",
|
||||
date: "2026-07-\(String(format: "%02d", page))",
|
||||
dayAmount: "12.30",
|
||||
dayPoints: 7,
|
||||
items: [EarningDetailItem(id: page, amount: "12.30", points: 7, typeLabel: "订单收益", createdAt: "10:00", withdrawLabel: "可提现")]
|
||||
items: (0 ..< count).map {
|
||||
EarningDetailItem(id: page * 100 + $0, amount: "12.30", points: 7, typeLabel: "订单收益", createdAt: "10:00", withdrawLabel: "可提现")
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -314,6 +424,7 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
func withdrawSendSMS() async throws {}
|
||||
|
||||
func withdrawApply(amount: String, smsCode: String) async throws {
|
||||
if shouldFailWithdrawApply { throw FakeWalletError.requestFailed }
|
||||
withdrawApplyRequests.append((amount, smsCode))
|
||||
}
|
||||
|
||||
@ -322,12 +433,20 @@ private final class FakeWalletPageService: WalletPageServing {
|
||||
}
|
||||
|
||||
func pointWithdrawApply(points: Int, remark: String) async throws {
|
||||
if shouldFailPointApply { throw FakeWalletError.requestFailed }
|
||||
pointApplyRequests.append(points)
|
||||
}
|
||||
|
||||
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")])
|
||||
pointWithdrawListPages.append(page)
|
||||
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||
return PointWithdrawListResponse(
|
||||
total: pagedRecordTotal,
|
||||
list: (0 ..< count).map {
|
||||
PointWithdrawItem(id: page * 100 + $0, points: 24, amount: 2, status: 1, createdAt: "2026-07-01")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -548,6 +548,91 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(record.imageCount, 1)
|
||||
XCTAssertEqual(record.videoCount, 1)
|
||||
XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
|
||||
XCTAssertEqual(detail.supplementRecords.first?.evidences.first?.fileURL, "https://example.com/supplement.jpg")
|
||||
}
|
||||
|
||||
func testSupplementRecordsPreserveMixedMediaOrderAndSummary() throws {
|
||||
let evidences = [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 1,
|
||||
fileTypeText: "图片",
|
||||
fileURL: "https://example.com/one.jpg",
|
||||
fileName: "one.jpg"
|
||||
),
|
||||
WildReportDetailEvidence(
|
||||
fileType: 2,
|
||||
fileTypeText: "视频",
|
||||
fileURL: "https://example.com/two.mp4",
|
||||
fileName: "two.mp4"
|
||||
),
|
||||
WildReportDetailEvidence(
|
||||
fileType: 5,
|
||||
fileTypeText: "图片",
|
||||
fileURL: "https://example.com/three.png",
|
||||
fileName: "three.png"
|
||||
)
|
||||
]
|
||||
let detail = WildReportDetailData(
|
||||
supplements: [
|
||||
WildReportDetailContent(
|
||||
type: "supplement",
|
||||
id: 7,
|
||||
desc: "补充现场材料",
|
||||
time: "2026-07-13 10:00:00",
|
||||
evidences: evidences
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
let record = try XCTUnwrap(detail.supplementRecords.first)
|
||||
|
||||
XCTAssertEqual(record.evidences.map(\.fileType), [1, 2, 5])
|
||||
XCTAssertEqual(record.evidences.map(\.fileURL), [
|
||||
"https://example.com/one.jpg",
|
||||
"https://example.com/two.mp4",
|
||||
"https://example.com/three.png"
|
||||
])
|
||||
XCTAssertEqual(record.imageCount, 2)
|
||||
XCTAssertEqual(record.videoCount, 1)
|
||||
XCTAssertTrue(record.hasMedia)
|
||||
XCTAssertEqual(record.summaryText, "补充现场材料 · 2张图片 · 1段视频")
|
||||
}
|
||||
|
||||
func testSupplementRecordsFallBackToReportContentsAndKeepMedia() throws {
|
||||
let detail = WildReportDetailData(
|
||||
supplements: [],
|
||||
reportContents: [
|
||||
WildReportDetailContent(
|
||||
type: "report",
|
||||
id: 1,
|
||||
desc: "初始举报",
|
||||
time: "2026-07-13 09:00:00"
|
||||
),
|
||||
WildReportDetailContent(
|
||||
type: "supplement",
|
||||
id: 2,
|
||||
desc: "补充视频",
|
||||
time: "2026-07-13 10:00:00",
|
||||
evidences: [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 2,
|
||||
fileTypeText: "视频",
|
||||
fileURL: "https://example.com/fallback.mp4",
|
||||
fileName: "fallback.mp4"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
let record = try XCTUnwrap(detail.supplementRecords.first)
|
||||
|
||||
XCTAssertEqual(detail.supplementRecords.count, 1)
|
||||
XCTAssertEqual(record.id, "2")
|
||||
XCTAssertEqual(record.evidences.map(\.fileURL), ["https://example.com/fallback.mp4"])
|
||||
XCTAssertEqual(record.imageCount, 0)
|
||||
XCTAssertEqual(record.videoCount, 1)
|
||||
XCTAssertEqual(record.summaryText, "补充视频 · 1段视频")
|
||||
}
|
||||
|
||||
func testSupplementRecordSequenceTitleStartsFromDisplayOrder() {
|
||||
|
||||
Reference in New Issue
Block a user