feat: update wallet punch point and report success
This commit is contained in:
259
suixinkanTests/PunchPointViewModelTests.swift
Normal file
259
suixinkanTests/PunchPointViewModelTests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user