feat: update wallet punch point and report success

This commit is contained in:
2026-07-09 14:27:18 +08:00
parent 2970f1514b
commit 43e6133c21
34 changed files with 6671 additions and 71 deletions

View File

@ -0,0 +1,259 @@
//
// PunchPointViewModelTests.swift
// suixinkanTests
//
import CoreLocation
import XCTest
@testable import suixinkan
/// ViewModel
@MainActor
final class PunchPointViewModelTests: XCTestCase {
func testFilterRefreshAndPaginationUseAndroidStatusAndPageSize() async {
let api = MockPunchPointAPI()
api.listResponses = [
PunchPointListResponse(total: 12, list: (1...10).map { item(id: Int64($0)) }),
PunchPointListResponse(total: 12, list: [item(id: 11), item(id: 12)]),
PunchPointListResponse(total: 1, list: [item(id: 21)]),
]
let viewModel = PunchPointListViewModel(currentScenicIdProvider: { 9 })
await viewModel.loadInitial(api: api)
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
await viewModel.selectFilter(.paused, api: api)
XCTAssertEqual(viewModel.items.map(\.id), [21])
XCTAssertEqual(api.listCalls.map(\.status), [0, 0, 2])
XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1])
XCTAssertEqual(api.listCalls.map(\.pageSize), [10, 10, 10])
XCTAssertEqual(api.listCalls.map(\.scenicAreaId), ["9", "9", "9"])
}
func testDeleteSuccessRemovesItemAndQRCodeValidationMatchesAndroid() async {
let api = MockPunchPointAPI()
api.listResponses = [PunchPointListResponse(total: 2, list: [
item(id: 1, status: 1, mpQrcode: "https://cdn/qr.png"),
item(id: 2, status: 3, mpQrcode: nil),
])]
let viewModel = PunchPointListViewModel(currentScenicIdProvider: { 9 })
var messages: [String] = []
var qrcode: String?
viewModel.onShowMessage = { messages.append($0) }
viewModel.onShowQRCode = { qrcode = $0 }
await viewModel.loadInitial(api: api)
viewModel.showQRCode(for: viewModel.items[1])
viewModel.showQRCode(for: PunchPointItem(id: 3, status: 1, mpQrcode: " "))
viewModel.showQRCode(for: viewModel.items[0])
await viewModel.delete(item: viewModel.items[0], api: api)
XCTAssertEqual(messages, ["未审核通过不可查看二维码", "暂无二维码"])
XCTAssertEqual(qrcode, "https://cdn/qr.png")
XCTAssertEqual(viewModel.items.map(\.id), [2])
XCTAssertEqual(api.deletedIds, [1])
}
func testFormValidationUploadAndCreateRequestBody() async {
let api = MockPunchPointAPI()
let uploader = MockPunchPointUploader(resultURL: "https://cdn/upload.jpg")
let location = MockPunchPointLocationProvider(reverseAddress: "地图地址")
let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
await viewModel.submit(api: api)
viewModel.updateName("湖边打卡")
await viewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 30.1, longitude: 120.2))
viewModel.updateDescription(String(repeating: "", count: 80))
await viewModel.addLocalImages([PunchPointImageState(data: Data([1, 2]), fileName: "a.jpg")], uploader: uploader)
await viewModel.submit(api: api)
XCTAssertEqual(messages.first, "请输入打卡点名称")
XCTAssertEqual(viewModel.description.count, 50)
XCTAssertEqual(uploader.uploads.map(\.moduleFileName), ["a.jpg"])
let request = api.addRequests.first
XCTAssertEqual(request?.scenicAreaId, "7")
XCTAssertEqual(request?.name, "湖边打卡")
XCTAssertEqual(request?.region.lat, 30.1)
XCTAssertEqual(request?.region.lot, 120.2)
XCTAssertEqual(request?.region.address, "地图地址")
XCTAssertEqual(request?.scenicSpotString, "地图地址")
XCTAssertEqual(request?.guideImages, ["https://cdn/upload.jpg"])
}
func testEditInitializationAndSubmitUsesEditAPI() async {
let api = MockPunchPointAPI()
let viewModel = PunchPointFormViewModel(mode: .edit(id: 66), currentScenicIdProvider: { 8 }, locationProvider: MockPunchPointLocationProvider())
var changedCoordinate: CLLocationCoordinate2D?
viewModel.onCoordinateChange = { changedCoordinate = $0 }
viewModel.initialize(with: PunchPointDetail(
id: 66,
name: "旧打卡点",
region: PunchPointRegion(lat: 31.1, lot: 121.1, address: "旧地址"),
description: "说明",
guideImages: ["https://cdn/old.jpg"]
))
viewModel.updateName("新打卡点")
await viewModel.submit(api: api)
XCTAssertEqual(changedCoordinate?.latitude, 31.1)
XCTAssertEqual(viewModel.images.first?.uploadedURL, "https://cdn/old.jpg")
XCTAssertEqual(api.editRequests.first?.id, 66)
XCTAssertEqual(api.editRequests.first?.name, "新打卡点")
XCTAssertEqual(api.editRequests.first?.guideImages, ["https://cdn/old.jpg"])
}
func testUploadingAndFailedImagesBlockSubmit() async {
let api = MockPunchPointAPI()
let slowUploader = SlowPunchPointUploader()
let location = MockPunchPointLocationProvider(reverseAddress: "地址")
let uploadingViewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
uploadingViewModel.updateName("上传中")
await uploadingViewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 1, longitude: 2))
let started = expectation(description: "upload started")
slowUploader.onStart = { started.fulfill() }
let task = Task {
await uploadingViewModel.addLocalImages([PunchPointImageState(data: Data([1]), fileName: "slow.jpg")], uploader: slowUploader)
}
await fulfillment(of: [started], timeout: 1)
var uploadMessages: [String] = []
uploadingViewModel.onShowMessage = { uploadMessages.append($0) }
await uploadingViewModel.submit(api: api)
task.cancel()
let failedViewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
failedViewModel.updateName("失败图")
await failedViewModel.selectCoordinate(CLLocationCoordinate2D(latitude: 1, longitude: 2))
await failedViewModel.addLocalImages(
[PunchPointImageState(data: Data([1]), fileName: "fail.jpg")],
uploader: MockPunchPointUploader(error: URLError(.cannotConnectToHost))
)
var failedMessages: [String] = []
failedViewModel.onShowMessage = { failedMessages.append($0) }
await failedViewModel.submit(api: api)
XCTAssertEqual(uploadMessages.last, "图片正在上传,请稍后提交")
XCTAssertEqual(failedMessages.last, "请先完成图片上传")
XCTAssertTrue(api.addRequests.isEmpty)
}
private func item(id: Int64, status: Int = 1, mpQrcode: String? = nil) -> PunchPointItem {
PunchPointItem(
id: id,
name: "\(id)",
status: status,
statusLabel: status == 1 ? "运营中" : "未审核",
region: PunchPointRegion(lat: 30, lot: 120, address: "地址\(id)"),
guideImages: ["https://cdn/\(id).jpg"],
mpQrcode: mpQrcode
)
}
}
@MainActor
private final class MockPunchPointAPI: PunchPointAPIProtocol {
var listResponses: [PunchPointListResponse] = []
var listCalls: [(scenicAreaId: String, status: Int, page: Int, pageSize: Int)] = []
var addRequests: [PunchPointSaveRequest] = []
var editRequests: [PunchPointSaveRequest] = []
var deletedIds: [Int64] = []
func list(scenicAreaId: String, status: Int, page: Int, pageSize: Int) async throws -> PunchPointListResponse {
listCalls.append((scenicAreaId, status, page, pageSize))
guard !listResponses.isEmpty else { return PunchPointListResponse() }
return listResponses.removeFirst()
}
func info(id: Int64) async throws -> PunchPointDetail {
PunchPointDetail(id: id)
}
func add(_ request: PunchPointSaveRequest) async throws {
addRequests.append(request)
}
func edit(_ request: PunchPointSaveRequest) async throws {
editRequests.append(request)
}
func delete(id: Int64) async throws {
deletedIds.append(id)
}
}
@MainActor
private final class MockPunchPointUploader: PunchPointImageUploading {
struct Upload {
let moduleFileName: String
let scenicId: Int
}
var uploads: [Upload] = []
let resultURL: String
let error: Error?
init(resultURL: String = "https://cdn/default.jpg", error: Error? = nil) {
self.resultURL = resultURL
self.error = error
}
func uploadPunchPointImage(
data: Data,
fileName: String,
scenicId: Int,
onProgress: @escaping (Int) -> Void
) async throws -> String {
uploads.append(Upload(moduleFileName: fileName, scenicId: scenicId))
onProgress(100)
if let error {
throw error
}
return resultURL
}
}
@MainActor
private final class SlowPunchPointUploader: PunchPointImageUploading {
var onStart: (() -> Void)?
func uploadPunchPointImage(
data: Data,
fileName: String,
scenicId: Int,
onProgress: @escaping (Int) -> Void
) async throws -> String {
onProgress(10)
onStart?()
try await Task.sleep(nanoseconds: 1_000_000_000)
onProgress(100)
return "https://cdn/slow.jpg"
}
}
private final class MockPunchPointLocationProvider: LocationProviding, @unchecked Sendable {
let snapshot: HomeLocationSnapshot
let reverseAddress: String
init(
snapshot: HomeLocationSnapshot = HomeLocationSnapshot(latitude: 30.2, longitude: 120.3, address: "当前位置"),
reverseAddress: String = "反查地址"
) {
self.snapshot = snapshot
self.reverseAddress = reverseAddress
}
func requestSnapshot() async throws -> HomeLocationSnapshot {
snapshot
}
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: snapshot.latitude, longitude: snapshot.longitude)
}
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
reverseAddress
}
}