feat: 更新素材管理和举报风险地图
This commit is contained in:
126
suixinkanTests/MaterialManagementAPITests.swift
Normal file
126
suixinkanTests/MaterialManagementAPITests.swift
Normal file
@ -0,0 +1,126 @@
|
||||
//
|
||||
// MaterialManagementAPITests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 素材管理 API 路径、参数与请求体测试。
|
||||
@MainActor
|
||||
final class MaterialManagementAPITests: XCTestCase {
|
||||
func testListBuildsAndroidQueryWithMaterialType() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":null}"#)])
|
||||
let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
_ = try await api.list(status: 4, keyword: "雪山", page: 2, pageSize: 10)
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "GET")
|
||||
XCTAssertEqual(request.url?.path, "/api/app/media-album/list")
|
||||
let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
|
||||
XCTAssertEqual(query?.first { $0.name == "status" }?.value, "4")
|
||||
XCTAssertEqual(query?.first { $0.name == "keyword" }?.value, "雪山")
|
||||
XCTAssertEqual(query?.first { $0.name == "type" }?.value, "1")
|
||||
XCTAssertEqual(query?.first { $0.name == "page" }?.value, "2")
|
||||
XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "10")
|
||||
}
|
||||
|
||||
func testListDecodesNullableOrderAmounts() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":{"avg_change":-1025,"avg_order_amount":null,"refund_total":null,"total_num":0}}"#)])
|
||||
let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.list(status: 4, keyword: nil, page: 1, pageSize: 10)
|
||||
|
||||
XCTAssertEqual(response.order?.avgOrderAmount, "0")
|
||||
XCTAssertEqual(response.order?.refundTotal, "0")
|
||||
XCTAssertEqual(response.order?.avgChange, -1025)
|
||||
XCTAssertEqual(response.order?.totalNum, 0)
|
||||
}
|
||||
|
||||
func testDetailOperationDeleteAndTagUseAndroidPaths() async throws {
|
||||
let detail = envelopeJSON(#"{"id":8,"status":1,"listing_status":1}"#)
|
||||
let empty = envelopeJSON("{}")
|
||||
let tag = envelopeJSON("23")
|
||||
let session = MockURLSession(responses: [detail, empty, empty, tag])
|
||||
let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
_ = try await api.detail(id: 8)
|
||||
try await api.setListingStatus(id: 8, listingStatus: 0)
|
||||
try await api.delete(id: 8)
|
||||
let tagId = try await api.addTag(name: "snow-tag")
|
||||
|
||||
XCTAssertEqual(tagId, 23)
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/media-album/detail")
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/media-album/operation")
|
||||
XCTAssertEqual(session.requests[2].url?.path, "/api/app/media-album/delete")
|
||||
XCTAssertEqual(session.requests[3].url?.path, "/api/app/media-album/add-tag")
|
||||
XCTAssertTrue(session.requests.prefix(3).allSatisfy { request in
|
||||
URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first { $0.name == "type" }?
|
||||
.value == "1"
|
||||
})
|
||||
}
|
||||
|
||||
func testUploadAndEditEncodeAndroidBodyKeys() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON("{}"), envelopeJSON("{}")])
|
||||
let api = MaterialManagementAPI(client: APIClient(environment: .testing, session: session))
|
||||
let request = MaterialUploadRequest(
|
||||
name: "素材A",
|
||||
mediaType: .image,
|
||||
mediaList: [
|
||||
MaterialUploadMediaItem(
|
||||
originalName: "a.jpg",
|
||||
ossUrl: "https://cdn/a.jpg",
|
||||
size: 1024,
|
||||
fileWidthSize: MaterialUploadFileSize(width: 1920, height: 1080)
|
||||
),
|
||||
],
|
||||
coverUrl: "https://cdn/c.jpg",
|
||||
coverSize: "512",
|
||||
scenicSpotId: 9,
|
||||
materialTag: "1,2"
|
||||
)
|
||||
|
||||
try await api.upload(request)
|
||||
try await api.edit(MaterialUploadRequest(
|
||||
id: 7,
|
||||
name: "素材B",
|
||||
mediaType: .video,
|
||||
mediaList: request.mediaList,
|
||||
coverUrl: request.coverUrl,
|
||||
coverSize: request.coverSize,
|
||||
scenicSpotId: request.scenicSpotId,
|
||||
materialTag: "3"
|
||||
))
|
||||
|
||||
let uploadBody = try bodyJSON(session.requests[0])
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/media-album/upload")
|
||||
XCTAssertEqual(uploadBody["name"] as? String, "素材A")
|
||||
XCTAssertEqual(uploadBody["type"] as? String, "1")
|
||||
XCTAssertEqual(uploadBody["media_type"] as? String, "1")
|
||||
XCTAssertNil(uploadBody["project_id"])
|
||||
XCTAssertEqual(uploadBody["material_tag"] as? String, "1,2")
|
||||
let media = (uploadBody["media_list"] as? [[String: Any]])?.first
|
||||
XCTAssertEqual(media?["original_name"] as? String, "a.jpg")
|
||||
XCTAssertEqual(media?["oss_url"] as? String, "https://cdn/a.jpg")
|
||||
|
||||
let editBody = try bodyJSON(session.requests[1])
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/media-album/edit")
|
||||
XCTAssertEqual(editBody["id"] as? Int, 7)
|
||||
XCTAssertEqual(editBody["type"] as? String, "1")
|
||||
XCTAssertEqual(editBody["media_type"] as? String, "2")
|
||||
XCTAssertNil(editBody["project_id"])
|
||||
}
|
||||
|
||||
private func envelopeJSON(_ dataJSON: String) -> Data {
|
||||
"""
|
||||
{"code":100000,"msg":"success","data":\(dataJSON)}
|
||||
""".data(using: .utf8)!
|
||||
}
|
||||
|
||||
private func bodyJSON(_ request: URLRequest) throws -> [String: Any] {
|
||||
try XCTUnwrap(JSONSerialization.jsonObject(with: try XCTUnwrap(request.httpBody)) as? [String: Any])
|
||||
}
|
||||
}
|
||||
258
suixinkanTests/MaterialManagementViewModelTests.swift
Normal file
258
suixinkanTests/MaterialManagementViewModelTests.swift
Normal file
@ -0,0 +1,258 @@
|
||||
//
|
||||
// MaterialManagementViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 素材管理 ViewModel 测试。
|
||||
@MainActor
|
||||
final class MaterialManagementViewModelTests: XCTestCase {
|
||||
func testListLoadsMoreSearchesAndMapsFilterStatus() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.listResponses = [
|
||||
MaterialListResponse(total: 12, list: (0 ..< 10).map { makeItem(id: $0) }, order: MaterialOrderInfo(totalNum: 1386, avgOrderAmount: "12", refundTotal: "3")),
|
||||
MaterialListResponse(total: 12, list: [makeItem(id: 10), makeItem(id: 11)]),
|
||||
MaterialListResponse(total: 1, list: [makeItem(id: 99)]),
|
||||
]
|
||||
let viewModel = MaterialListViewModel()
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 8, api: api)
|
||||
await viewModel.selectFilter(.pending, api: api)
|
||||
|
||||
XCTAssertEqual(api.listCalls.map(\.status), [4, 4, 0])
|
||||
XCTAssertEqual(api.listCalls.map(\.page), [1, 2, 1])
|
||||
XCTAssertEqual(viewModel.items.map(\.id), [99])
|
||||
}
|
||||
|
||||
func testRejectedMaterialCannotToggleListing() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 1, status: 0, listingStatus: 0)])]
|
||||
let viewModel = MaterialListViewModel()
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.toggleListing(materialId: 1, checked: true, api: api)
|
||||
|
||||
XCTAssertEqual(messages.last, "审核通过才可以操作上下架")
|
||||
XCTAssertTrue(api.operationCalls.isEmpty)
|
||||
}
|
||||
|
||||
func testApprovedMaterialToggleUpdatesLocalItem() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.listResponses = [MaterialListResponse(total: 1, list: [makeItem(id: 2, status: 1, listingStatus: 0)])]
|
||||
let viewModel = MaterialListViewModel()
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.toggleListing(materialId: 2, checked: true, api: api)
|
||||
|
||||
XCTAssertEqual(api.operationCalls.first?.id, 2)
|
||||
XCTAssertEqual(api.operationCalls.first?.listingStatus, 1)
|
||||
XCTAssertEqual(viewModel.items.first?.listingStatus, 1)
|
||||
}
|
||||
|
||||
func testDetailDeleteCallsCallback() async {
|
||||
let api = MockMaterialAPI()
|
||||
let viewModel = MaterialDetailViewModel(materialId: 7)
|
||||
var deletedId: Int?
|
||||
viewModel.onDeleted = { deletedId = $0 }
|
||||
|
||||
await viewModel.delete(api: api)
|
||||
|
||||
XCTAssertEqual(api.deletedIds, [7])
|
||||
XCTAssertEqual(deletedId, 7)
|
||||
}
|
||||
|
||||
func testUploadValidationMessages() async {
|
||||
let api = MockMaterialAPI()
|
||||
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
await viewModel.submit(api: api)
|
||||
viewModel.updateMaterialName("素材")
|
||||
await viewModel.submit(api: api)
|
||||
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: MockMaterialUploader(urls: ["https://cdn/a.jpg"]))
|
||||
await viewModel.submit(api: api)
|
||||
await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: MockMaterialUploader(urls: ["https://cdn/c.jpg"]))
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertEqual(messages.prefix(4), [
|
||||
"请输入素材名称",
|
||||
"请至少上传一个素材",
|
||||
"请上传封面图片",
|
||||
"请选择关联打卡点",
|
||||
])
|
||||
XCTAssertNil(api.lastUploadRequest)
|
||||
}
|
||||
|
||||
func testUploadSuccessBuildsMaterialRequestWithTags() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
||||
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg", "https://cdn/c.jpg"])
|
||||
var didSucceed = false
|
||||
viewModel.onSubmitSuccess = { didSucceed = true }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
viewModel.updateMaterialName("素材")
|
||||
viewModel.selectScenicSpot(ScenicSpotItem(id: 9, name: "打卡点"))
|
||||
viewModel.updateTagInput("snow-tag")
|
||||
await viewModel.addTag(api: api)
|
||||
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
|
||||
await viewModel.setCoverImage(makeUploadItem(fileName: "c.jpg"), uploader: uploader)
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertTrue(didSucceed)
|
||||
XCTAssertEqual(api.lastUploadRequest?.name, "素材")
|
||||
XCTAssertEqual(api.lastUploadRequest?.type, "1")
|
||||
XCTAssertEqual(api.lastUploadRequest?.mediaType, "1")
|
||||
XCTAssertEqual(api.lastUploadRequest?.mediaList.first?.ossUrl, "https://cdn/a.jpg")
|
||||
XCTAssertEqual(api.lastUploadRequest?.coverUrl, "https://cdn/c.jpg")
|
||||
XCTAssertEqual(api.lastUploadRequest?.scenicSpotId, 9)
|
||||
XCTAssertEqual(api.lastUploadRequest?.materialTag, "42")
|
||||
XCTAssertNil(api.lastUploadRequest?.id)
|
||||
}
|
||||
|
||||
func testEditLoadsDetailAndBuildsEditRequest() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
||||
api.detailResponse = try! decodeDetail("""
|
||||
{
|
||||
"id": 11,
|
||||
"name": "旧素材",
|
||||
"status": 1,
|
||||
"listing_status": 1,
|
||||
"cover_url": "https://cdn/cover.jpg",
|
||||
"cover_size": 22,
|
||||
"scenic_spot_id": 9,
|
||||
"material_tag": [{"id": 3, "name": "cloudy"}],
|
||||
"media_list": [{
|
||||
"id": 5,
|
||||
"original_name": "old.jpg",
|
||||
"oss_url": "https://cdn/old.jpg",
|
||||
"type": 1,
|
||||
"size": 10,
|
||||
"width": 100,
|
||||
"height": 80
|
||||
}]
|
||||
}
|
||||
""")
|
||||
let viewModel = MaterialFormViewModel(mode: .edit(id: 11), currentScenicIdProvider: { 3 })
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.submit(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.materialName, "旧素材")
|
||||
XCTAssertEqual(viewModel.selectedScenicSpot?.id, 9)
|
||||
XCTAssertEqual(viewModel.uploadedMediaList.first?.ossUrl, "https://cdn/old.jpg")
|
||||
XCTAssertEqual(api.lastEditRequest?.id, 11)
|
||||
XCTAssertEqual(api.lastEditRequest?.type, "1")
|
||||
XCTAssertEqual(api.lastEditRequest?.materialTag, "3")
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材管理 API mock。
|
||||
/// 素材管理 API mock。
|
||||
@MainActor
|
||||
private final class MockMaterialAPI: MaterialManagementServing {
|
||||
var listResponses: [MaterialListResponse] = []
|
||||
var listCalls: [(status: Int, keyword: String?, page: Int, pageSize: Int)] = []
|
||||
var operationCalls: [(id: Int, listingStatus: Int)] = []
|
||||
var deletedIds: [Int] = []
|
||||
var tagId = 42
|
||||
var lastUploadRequest: MaterialUploadRequest?
|
||||
var lastEditRequest: MaterialUploadRequest?
|
||||
var spotResponse = ScenicSpotListResponse(list: [], total: 0)
|
||||
var detailResponse = try! decodeDetail(#"{"id":1,"status":1,"listing_status":1}"#)
|
||||
|
||||
func list(status: Int, keyword: String?, page: Int, pageSize: Int) async throws -> MaterialListResponse {
|
||||
listCalls.append((status, keyword, page, pageSize))
|
||||
guard !listResponses.isEmpty else { return MaterialListResponse() }
|
||||
return listResponses.removeFirst()
|
||||
}
|
||||
|
||||
func detail(id: Int) async throws -> MaterialDetail {
|
||||
detailResponse
|
||||
}
|
||||
|
||||
func setListingStatus(id: Int, listingStatus: Int) async throws {
|
||||
operationCalls.append((id, listingStatus))
|
||||
}
|
||||
|
||||
func delete(id: Int) async throws {
|
||||
deletedIds.append(id)
|
||||
}
|
||||
|
||||
func addTag(name: String) async throws -> Int {
|
||||
tagId
|
||||
}
|
||||
|
||||
func upload(_ request: MaterialUploadRequest) async throws {
|
||||
lastUploadRequest = request
|
||||
}
|
||||
|
||||
func edit(_ request: MaterialUploadRequest) async throws {
|
||||
lastEditRequest = request
|
||||
}
|
||||
|
||||
func scenicSpotListAll(scenicId: String) async throws -> ScenicSpotListResponse {
|
||||
spotResponse
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材管理 OSS 上传 mock。
|
||||
/// 素材管理 OSS 上传 mock。
|
||||
@MainActor
|
||||
private final class MockMaterialUploader: MaterialOSSUploading {
|
||||
private var urls: [String]
|
||||
|
||||
init(urls: [String]) {
|
||||
self.urls = urls
|
||||
}
|
||||
|
||||
func uploadMaterialFile(
|
||||
data: Data,
|
||||
fileName: String,
|
||||
fileType: Int,
|
||||
scenicId: Int,
|
||||
onProgress: @escaping (Int) -> Void
|
||||
) async throws -> String {
|
||||
onProgress(100)
|
||||
return urls.isEmpty ? "https://cdn/default.jpg" : urls.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private func makeItem(id: Int, status: Int = 1, listingStatus: Int = 0) -> MaterialItem {
|
||||
MaterialItem(
|
||||
id: id,
|
||||
name: "素材\(id)",
|
||||
coverUrl: "",
|
||||
createdAt: "",
|
||||
status: status,
|
||||
downloadCount: 0,
|
||||
likesCount: 0,
|
||||
collectCount: 0,
|
||||
shareCount: 0,
|
||||
listingStatus: listingStatus
|
||||
)
|
||||
}
|
||||
|
||||
private func makeUploadItem(fileName: String, ossUrl: String = "") -> MaterialUploadedMediaItem {
|
||||
MaterialUploadedMediaItem(
|
||||
data: Data(repeating: 1, count: 10),
|
||||
fileName: fileName,
|
||||
size: 10,
|
||||
width: 1,
|
||||
height: 1,
|
||||
ossUrl: ossUrl
|
||||
)
|
||||
}
|
||||
|
||||
private func decodeDetail(_ json: String) throws -> MaterialDetail {
|
||||
try JSONDecoder().decode(MaterialDetail.self, from: Data(json.utf8))
|
||||
}
|
||||
55
suixinkanTests/MediaPreviewModelsTests.swift
Normal file
55
suixinkanTests/MediaPreviewModelsTests.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// MediaPreviewModelsTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 通用媒体预览模型测试。
|
||||
final class MediaPreviewModelsTests: XCTestCase {
|
||||
|
||||
func testClampedStartIndexKeepsIndexInBounds() {
|
||||
XCTAssertEqual(MediaPreviewItem.clampedStartIndex(-3, itemCount: 4), 0)
|
||||
XCTAssertEqual(MediaPreviewItem.clampedStartIndex(2, itemCount: 4), 2)
|
||||
XCTAssertEqual(MediaPreviewItem.clampedStartIndex(8, itemCount: 4), 3)
|
||||
XCTAssertEqual(MediaPreviewItem.clampedStartIndex(8, itemCount: 0), 0)
|
||||
}
|
||||
|
||||
func testRemoteImageAndVideoURLConstructionFiltersInvalidText() {
|
||||
let image = MediaPreviewItem.remoteImage(" https://cdn.example.com/a.jpg ")
|
||||
let video = MediaPreviewItem.remoteVideo("https://cdn.example.com/a.mp4")
|
||||
|
||||
XCTAssertEqual(image?.mediaType, .image)
|
||||
XCTAssertEqual(video?.mediaType, .video)
|
||||
XCTAssertNil(MediaPreviewItem.remoteImage(""))
|
||||
XCTAssertNil(MediaPreviewItem.remoteImage(" "))
|
||||
XCTAssertNil(MediaPreviewItem.remoteVideo("file:///tmp/a.mp4"))
|
||||
XCTAssertNil(MediaPreviewItem.remoteVideo("not a url"))
|
||||
}
|
||||
|
||||
func testLocalImageAndVideoTypeRecognition() {
|
||||
let localImage = MediaPreviewItem.localImage(url: URL(fileURLWithPath: "/tmp/a.jpg"))
|
||||
let memoryImage = MediaPreviewItem.image(UIImage())
|
||||
let localVideo = MediaPreviewItem.localVideo(url: URL(fileURLWithPath: "/tmp/a.mp4"))
|
||||
|
||||
XCTAssertTrue(localImage.isImage)
|
||||
XCTAssertTrue(memoryImage.isImage)
|
||||
XCTAssertTrue(localVideo.isVideo)
|
||||
XCTAssertEqual(localVideo.mediaType, .video)
|
||||
}
|
||||
|
||||
func testCurrentItemReadsClampedItemFromMixedMedia() {
|
||||
let items = [
|
||||
MediaPreviewItem.localImage(url: URL(fileURLWithPath: "/tmp/a.jpg")),
|
||||
MediaPreviewItem.localVideo(url: URL(fileURLWithPath: "/tmp/a.mp4")),
|
||||
MediaPreviewItem.remoteImage("https://cdn.example.com/b.jpg")!,
|
||||
]
|
||||
|
||||
XCTAssertEqual(MediaPreviewItem.currentItem(in: items, startIndex: -1)?.mediaType, .image)
|
||||
XCTAssertEqual(MediaPreviewItem.currentItem(in: items, startIndex: 1)?.mediaType, .video)
|
||||
XCTAssertEqual(MediaPreviewItem.currentItem(in: items, startIndex: 99)?.mediaType, .image)
|
||||
XCTAssertNil(MediaPreviewItem.currentItem(in: [], startIndex: 0))
|
||||
}
|
||||
}
|
||||
@ -13,13 +13,15 @@ final class MockLocationProvider: LocationProviding, @unchecked Sendable {
|
||||
var reverseGeocodeResult = "北京市东城区"
|
||||
var requestSnapshotError: Error?
|
||||
var requestCoordinateError: Error?
|
||||
var requestedAccuracies: [CLLocationAccuracy] = []
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
if let requestSnapshotError { throw requestSnapshotError }
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||||
requestedAccuracies.append(desiredAccuracy)
|
||||
if let requestCoordinateError { throw requestCoordinateError }
|
||||
return coordinate
|
||||
}
|
||||
|
||||
@ -259,7 +259,7 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(navigationController.topViewController is ProfileSpaceSettingsViewController)
|
||||
}
|
||||
|
||||
func testSystemSettingsRouteStillSelectsProfileTab() {
|
||||
func testSystemSettingsRoutePushesSettingPage() {
|
||||
AppStore.shared.currentScenicId = 11
|
||||
let tabBarController = MainTabBarController()
|
||||
tabBarController.loadViewIfNeeded()
|
||||
@ -273,7 +273,8 @@ final class ProfileSpaceSettingsViewModelTests: XCTestCase {
|
||||
|
||||
HomeRouteHandler.open(uri: "system_settings", from: root, storeItem: nil)
|
||||
|
||||
XCTAssertEqual(tabBarController.selectedIndex, 4)
|
||||
XCTAssertEqual(tabBarController.selectedIndex, 0)
|
||||
XCTAssertTrue(navigationController.topViewController is SettingViewController)
|
||||
}
|
||||
|
||||
private func makeViewModel() -> ProfileSpaceSettingsViewModel {
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 样片管理 API 测试。
|
||||
@MainActor
|
||||
final class SampleManagementAPITests: XCTestCase {
|
||||
func testListBuildsAndroidQuery() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":null}"#)])
|
||||
@ -26,6 +26,18 @@ final class SampleManagementAPITests: XCTestCase {
|
||||
XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "10")
|
||||
}
|
||||
|
||||
func testListDecodesNullableOrderAmounts() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON(#"{"total":0,"list":[],"order":{"avg_change":-1025,"avg_order_amount":null,"refund_total":null,"total_num":0}}"#)])
|
||||
let api = SampleManagementAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.list(status: 4, keyword: nil, page: 1, pageSize: 10)
|
||||
|
||||
XCTAssertEqual(response.order?.avgOrderAmount, "0")
|
||||
XCTAssertEqual(response.order?.refundTotal, "0")
|
||||
XCTAssertEqual(response.order?.avgChange, -1025)
|
||||
XCTAssertEqual(response.order?.totalNum, 0)
|
||||
}
|
||||
|
||||
func testDetailAndOperationUseSampleType() async throws {
|
||||
let detail = envelopeJSON(#"{"id":8,"status":1,"listing_status":1}"#)
|
||||
let empty = envelopeJSON("{}")
|
||||
|
||||
91
suixinkanTests/SettingViewModelTests.swift
Normal file
91
suixinkanTests/SettingViewModelTests.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// SettingViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 设置中心 ViewModel 测试。
|
||||
final class SettingViewModelTests: XCTestCase {
|
||||
func testAppVersionUsesClientInfoFormatting() {
|
||||
let viewModel = SettingViewModel(
|
||||
infoDictionary: [
|
||||
"CFBundleShortVersionString": "1.2",
|
||||
"CFBundleVersion": "9",
|
||||
],
|
||||
environment: .testing
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.appVersion, "1.2.9")
|
||||
}
|
||||
|
||||
func testAppDownloadURLUsesEnvironmentBaseURL() {
|
||||
let viewModel = SettingViewModel(infoDictionary: [:], environment: .testing)
|
||||
|
||||
XCTAssertEqual(viewModel.appDownloadURL.absoluteString, "https://api-test.zhifly.cn/h5/app/download")
|
||||
}
|
||||
|
||||
func testVersionCodeComparison() {
|
||||
XCTAssertTrue(SettingViewModel.isNewerVersionCode(11, than: 10))
|
||||
XCTAssertFalse(SettingViewModel.isNewerVersionCode(10, than: 10))
|
||||
XCTAssertFalse(SettingViewModel.isNewerVersionCode(9, than: 10))
|
||||
XCTAssertFalse(SettingViewModel.isNewerVersionCode(11, than: nil))
|
||||
}
|
||||
|
||||
func testAgreementDestinationsMapToAndroidPaths() {
|
||||
let viewModel = SettingViewModel(infoDictionary: [:], environment: .testing)
|
||||
|
||||
let about = viewModel.agreementDestination(for: .aboutUs)
|
||||
let userAgreement = viewModel.agreementDestination(for: .userAgreement)
|
||||
let privacy = viewModel.agreementDestination(for: .privacyPolicy)
|
||||
|
||||
XCTAssertEqual(about.title, "关于我们")
|
||||
XCTAssertEqual(about.url.absoluteString, "https://api-test.zhifly.cn/h5/app/about-us")
|
||||
XCTAssertEqual(userAgreement.title, "用户协议")
|
||||
XCTAssertEqual(userAgreement.url.absoluteString, "https://api-test.zhifly.cn/h5/app/user-agreement")
|
||||
XCTAssertEqual(privacy.title, "隐私政策")
|
||||
XCTAssertEqual(privacy.url.absoluteString, "https://api-test.zhifly.cn/h5/app/privacy-policy")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testCheckLatestVersionMarksNewVersionWhenVersionCodeIsHigher() async throws {
|
||||
let json = """
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"version": "1.0.1",
|
||||
"operator": "admin",
|
||||
"description": "update",
|
||||
"status": 1,
|
||||
"created_at": "2026-07-09 00:00:00",
|
||||
"updated_at": "2026-07-09 00:00:00",
|
||||
"deleted_at": null,
|
||||
"version_code": 8,
|
||||
"apk_url": "https://example.com/app.apk",
|
||||
"is_force_update": 0,
|
||||
"approve_notes": ""
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let session = MockURLSession(responses: [json])
|
||||
let client = APIClient(environment: .testing, session: session)
|
||||
let api = SettingAPI(client: client)
|
||||
let viewModel = SettingViewModel(
|
||||
infoDictionary: [
|
||||
"CFBundleShortVersionString": "1.0.0",
|
||||
"CFBundleVersion": "7",
|
||||
],
|
||||
environment: .testing
|
||||
)
|
||||
|
||||
await viewModel.checkLatestVersion(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.latestVersion?.versionCode, 8)
|
||||
XCTAssertTrue(viewModel.hasNewVersion)
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/app/latest-version")
|
||||
}
|
||||
}
|
||||
@ -8,36 +8,8 @@ import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 举报摄影师模块测试,覆盖 Mock 数据、接口类型、提交校验、列表筛选和补充证据。
|
||||
/// 举报摄影师模块测试,覆盖接口类型、提交校验、列表筛选和补充证据。
|
||||
final class WildPhotographerReportTests: XCTestCase {
|
||||
func testSeedDataLoadsReportsAndRiskPoints() {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
|
||||
XCTAssertFalse(viewModel.reports.isEmpty)
|
||||
XCTAssertFalse(viewModel.riskPoints.isEmpty)
|
||||
XCTAssertEqual(viewModel.reports.count, WildPhotographerReportMockStore.seedReports.count)
|
||||
}
|
||||
|
||||
func testSubmitReportGeneratesDynamicIDAndInsertsFirst() {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
let beforeCount = viewModel.reports.count
|
||||
|
||||
let record = viewModel.submitReport(
|
||||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||||
description: "测试举报说明",
|
||||
location: "测试景区 · 测试点位",
|
||||
contact: "13800138000",
|
||||
imageCount: 1,
|
||||
videoCount: 0
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.reports.count, beforeCount + 1)
|
||||
XCTAssertEqual(viewModel.reports.first?.id, record.id)
|
||||
XCTAssertTrue(record.id.hasPrefix("JB"))
|
||||
XCTAssertEqual(record.status, .pending)
|
||||
XCTAssertEqual(record.phoneNumber, "13800138000")
|
||||
}
|
||||
|
||||
func testReportTypesAPILoadsRemoteOptions() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
@ -68,6 +40,75 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
])
|
||||
}
|
||||
|
||||
func testReportCopyAPILoadsNoticeAndRuleGroups() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"notice": ["需实名登录", "请尽量上传清晰证据"],
|
||||
"rule_groups": [
|
||||
{
|
||||
"title": "可举报行为",
|
||||
"items": ["疑似打野摄影师主动揽客"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": "2026-07-09 10:15:17"
|
||||
}
|
||||
""".utf8)
|
||||
let session = MockURLSession(responses: [json])
|
||||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
let response = try await api.reportCopy()
|
||||
|
||||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/copy")
|
||||
XCTAssertEqual(response.notice, ["需实名登录", "请尽量上传清晰证据"])
|
||||
XCTAssertEqual(response.ruleGroups, [
|
||||
WildReportRuleGroup(title: "可举报行为", items: ["疑似打野摄影师主动揽客"])
|
||||
])
|
||||
}
|
||||
|
||||
func testHomeViewModelLoadsReportCopyAndDisplayRules() async {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportCopyResponse = WildReportCopyResponse(
|
||||
notice: ["需实名登录", "请尽量上传清晰证据"],
|
||||
ruleGroups: [WildReportRuleGroup(title: "材料要求", items: ["尽量提供现场图片"])]
|
||||
)
|
||||
|
||||
await viewModel.loadReportCopy(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.noticeItems, ["需实名登录", "请尽量上传清晰证据"])
|
||||
XCTAssertEqual(viewModel.ruleGroups, [WildReportRuleGroup(title: "材料要求", items: ["尽量提供现场图片"])])
|
||||
XCTAssertTrue(viewModel.shouldShowNoticeModule)
|
||||
XCTAssertTrue(viewModel.shouldShowRuleButton)
|
||||
}
|
||||
|
||||
func testHomeViewModelHidesNoticeModuleWhenCopyIsEmpty() async {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportCopyResponse = WildReportCopyResponse(notice: [" "], ruleGroups: [WildReportRuleGroup(title: " ", items: [])])
|
||||
|
||||
await viewModel.loadReportCopy(api: api)
|
||||
|
||||
XCTAssertTrue(viewModel.noticeItems.isEmpty)
|
||||
XCTAssertTrue(viewModel.ruleGroups.isEmpty)
|
||||
XCTAssertFalse(viewModel.shouldShowNoticeModule)
|
||||
XCTAssertFalse(viewModel.shouldShowRuleButton)
|
||||
}
|
||||
|
||||
func testHomeViewModelShowsNoticeWithoutRuleButtonWhenRulesAreEmpty() async {
|
||||
let viewModel = WildPhotographerReportHomeViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportCopyResponse = WildReportCopyResponse(notice: ["需实名登录"], ruleGroups: [])
|
||||
|
||||
await viewModel.loadReportCopy(api: api)
|
||||
|
||||
XCTAssertTrue(viewModel.shouldShowNoticeModule)
|
||||
XCTAssertFalse(viewModel.shouldShowRuleButton)
|
||||
}
|
||||
|
||||
func testReportListAPILoadsRemoteReports() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
@ -239,7 +280,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
|
||||
}
|
||||
|
||||
func testRiskMapAPISendsRequestBodyAndDecodesMarkers() async throws {
|
||||
func testRiskMapAPISendsGETQueryAndDecodesMarkers() async throws {
|
||||
let json = Data("""
|
||||
{
|
||||
"code": 100000,
|
||||
@ -291,13 +332,13 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
))
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.httpMethod, "GET")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/risk-map")
|
||||
let bodyData = try XCTUnwrap(request.httpBody)
|
||||
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
|
||||
XCTAssertEqual(body["scenic_id"] as? Int, 128)
|
||||
XCTAssertEqual(body["latitude"] as? Double, 43.3779)
|
||||
XCTAssertEqual(body["longitude"] as? Double, 84.0217)
|
||||
XCTAssertNil(request.httpBody)
|
||||
let queryItems = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
XCTAssertEqual(queryItems.first(where: { $0.name == "scenic_id" })?.value, "128")
|
||||
XCTAssertEqual(queryItems.first(where: { $0.name == "latitude" })?.value, "43.3779")
|
||||
XCTAssertEqual(queryItems.first(where: { $0.name == "longitude" })?.value, "84.0217")
|
||||
XCTAssertEqual(response.scenicName, "伊犁那拉提景区-5A")
|
||||
XCTAssertEqual(response.greenMarkerTip, "绿色为景区内摄影师")
|
||||
XCTAssertEqual(response.markers.map(\.markerType), ["red", "yellow"])
|
||||
@ -428,10 +469,10 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
WildReportType(value: 2, label: "疑似打野"),
|
||||
WildReportType(value: 4, label: "其他")
|
||||
])
|
||||
XCTAssertEqual(viewModel.selectedReportType, .defaultSelected)
|
||||
XCTAssertEqual(viewModel.selectedReportType, WildReportType(value: 1, label: "私下收款"))
|
||||
}
|
||||
|
||||
func testSubmitViewModelFallsBackWhenReportTypesFail() async {
|
||||
func testSubmitViewModelKeepsReportTypesEmptyWhenReportTypesFail() async {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
@ -441,43 +482,61 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
|
||||
await viewModel.loadReportTypes(api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.reportTypes, WildReportType.fallbackOptions)
|
||||
XCTAssertEqual(viewModel.selectedReportType, .defaultSelected)
|
||||
XCTAssertEqual(messages.last, "举报类型获取失败,已使用默认类型")
|
||||
XCTAssertTrue(viewModel.reportTypes.isEmpty)
|
||||
XCTAssertNil(viewModel.selectedReportType)
|
||||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||||
}
|
||||
|
||||
func testSubmitFailsWithoutEvidence() {
|
||||
func testSubmitFailsWithoutEvidence() async {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.updateDescription("有说明")
|
||||
viewModel.applyResolvedLocation("测试位置")
|
||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))
|
||||
|
||||
viewModel.submitReport()
|
||||
await viewModel.submitReport(
|
||||
api: MockWildPhotographerReportAPI(),
|
||||
uploader: MockWildReportAttachmentUploader(),
|
||||
scenicId: 9,
|
||||
scenicName: "测试景区"
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
|
||||
XCTAssertNil(viewModel.submitResult)
|
||||
}
|
||||
|
||||
func testSubmitFailsWithoutDescription() {
|
||||
func testSubmitFailsWithoutDescription() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.addImage()
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||||
viewModel.updateDescription(" ")
|
||||
viewModel.applyResolvedLocation("测试位置")
|
||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))
|
||||
|
||||
viewModel.submitReport()
|
||||
await viewModel.submitReport(
|
||||
api: MockWildPhotographerReportAPI(),
|
||||
uploader: MockWildReportAttachmentUploader(),
|
||||
scenicId: 9,
|
||||
scenicName: "测试景区"
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.validationMessage, "请填写举报说明。")
|
||||
XCTAssertNil(viewModel.submitResult)
|
||||
}
|
||||
|
||||
func testSubmitFailsWithoutConfirmedLocation() {
|
||||
func testSubmitFailsWithoutConfirmedLocation() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.addImage()
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||||
viewModel.updateDescription("测试说明")
|
||||
|
||||
viewModel.submitReport()
|
||||
await viewModel.submitReport(
|
||||
api: MockWildPhotographerReportAPI(),
|
||||
uploader: MockWildReportAttachmentUploader(),
|
||||
scenicId: 9,
|
||||
scenicName: "测试景区"
|
||||
)
|
||||
|
||||
XCTAssertEqual(viewModel.validationMessage, "请先更新举报位置后再提交举报。")
|
||||
XCTAssertNil(viewModel.submitResult)
|
||||
@ -488,6 +547,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
let uploader = MockWildReportAttachmentUploader()
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||||
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
|
||||
viewModel.updateDescription("测试说明")
|
||||
@ -497,7 +557,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
|
||||
XCTAssertNil(viewModel.validationMessage)
|
||||
XCTAssertNotNil(viewModel.submitResult)
|
||||
XCTAssertEqual(homeViewModel.reports.first?.id, viewModel.submitResult?.record.id)
|
||||
XCTAssertEqual(viewModel.submitResult?.record.id, "")
|
||||
XCTAssertEqual(uploader.uploadedFileNames, ["scene.jpg", "payment.jpg"])
|
||||
XCTAssertEqual(api.submitRequests.first?.scenicId, 9)
|
||||
XCTAssertEqual(api.submitRequests.first?.reportType, 2)
|
||||
@ -512,9 +572,10 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
func testPaymentScreenshotsDoNotReplaceRequiredEvidence() async throws {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.addPaymentImages([try makeAttachment(kind: .payment, fileName: "payment.jpg")])
|
||||
viewModel.updateDescription("测试说明")
|
||||
viewModel.applyResolvedLocation("测试景区 · 测试点")
|
||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
||||
|
||||
await viewModel.submitReport(
|
||||
api: MockWildPhotographerReportAPI(),
|
||||
@ -546,9 +607,10 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
let uploader = MockWildReportAttachmentUploader()
|
||||
uploader.error = OSSUploadError.emptyFile
|
||||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||||
viewModel.updateDescription("测试说明")
|
||||
viewModel.applyResolvedLocation("测试景区 · 测试点")
|
||||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
||||
|
||||
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
|
||||
|
||||
@ -556,23 +618,6 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.validationMessage, OSSUploadError.emptyFile.localizedDescription)
|
||||
}
|
||||
|
||||
func testListFilterReturnsMatchingStatus() {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||||
|
||||
viewModel.selectFilter(.pending)
|
||||
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .pending })
|
||||
|
||||
viewModel.selectFilter(.processing)
|
||||
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .processing })
|
||||
|
||||
viewModel.selectFilter(.processed)
|
||||
XCTAssertTrue(viewModel.filteredReports.allSatisfy { $0.status == .processed })
|
||||
|
||||
viewModel.selectFilter(.all)
|
||||
XCTAssertEqual(viewModel.filteredReports.count, homeViewModel.reports.count)
|
||||
}
|
||||
|
||||
func testListInitialLoadUsesAllStatusAndFirstPage() async {
|
||||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||||
@ -668,9 +713,6 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(api.supplementRequests.first?.id, 32)
|
||||
XCTAssertEqual(api.supplementRequests.first?.desc, "补充说明")
|
||||
XCTAssertEqual(api.supplementRequests.first?.evidences?.map(\.fileType), [1, 2])
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.text, "补充说明")
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.imageCount, 1)
|
||||
XCTAssertEqual(homeViewModel.supplementaryEvidences(for: record.id).first?.videoCount, 1)
|
||||
}
|
||||
|
||||
func testSupplementEvidenceFailsWithoutServerID() async {
|
||||
@ -757,10 +799,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRiskMapViewModelRejectsInvalidScenicID() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let viewModel = WildReportRiskMapViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
@ -776,23 +815,23 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsWithLocationAndMapsMarkerTypes() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let viewModel = WildReportRiskMapViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
let locationProvider = MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217))
|
||||
|
||||
await viewModel.loadInitial(
|
||||
api: api,
|
||||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||||
locationProvider: locationProvider,
|
||||
scenicId: 128,
|
||||
scenicName: "测试景区"
|
||||
)
|
||||
|
||||
XCTAssertEqual(api.riskMapRequests.count, 1)
|
||||
XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
|
||||
XCTAssertEqual(api.riskMapRequests.first?.latitude, 43.3779)
|
||||
XCTAssertEqual(api.riskMapRequests.first?.longitude, 84.0217)
|
||||
XCTAssertEqual(locationProvider.requestedAccuracies.last, kCLLocationAccuracyHundredMeters)
|
||||
XCTAssertEqual(viewModel.scenicAreaName, "伊犁那拉提景区-5A")
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .selfLocation })
|
||||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .wildPhotographer })
|
||||
@ -807,10 +846,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsWithoutLocationWhenLocationFails() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let viewModel = WildReportRiskMapViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
|
||||
@ -827,10 +863,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRiskMapViewModelLoadsMarkerDetailAndUpdatesSelectedClue() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let viewModel = WildReportRiskMapViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
api.markerDetailResponses = [
|
||||
@ -867,10 +900,7 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRiskMapViewModelMarkerDetailFailureKeepsSummary() async {
|
||||
let viewModel = WildReportRiskMapViewModel(
|
||||
record: makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord(),
|
||||
riskPoints: []
|
||||
)
|
||||
let viewModel = WildReportRiskMapViewModel()
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.riskMapResponses = [makeRiskMapResponse()]
|
||||
api.markerDetailError = URLError(.badServerResponse)
|
||||
@ -946,6 +976,8 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
@MainActor
|
||||
private final class MockWildPhotographerReportAPI: WildPhotographerReportServing {
|
||||
var reportTypeResponse = WildReportTypeListResponse(list: [])
|
||||
var reportCopyResponse = WildReportCopyResponse()
|
||||
var reportCopyError: Error?
|
||||
var error: Error?
|
||||
var submitRequests: [WildReportSubmitRequest] = []
|
||||
var reportListResponses: [WildReportListResponse] = []
|
||||
@ -968,6 +1000,11 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
return reportTypeResponse
|
||||
}
|
||||
|
||||
func reportCopy() async throws -> WildReportCopyResponse {
|
||||
if let reportCopyError { throw reportCopyError }
|
||||
return reportCopyResponse
|
||||
}
|
||||
|
||||
func submitReport(_ request: WildReportSubmitRequest) async throws {
|
||||
if let error { throw error }
|
||||
submitRequests.append(request)
|
||||
@ -1018,11 +1055,24 @@ private enum MockRiskMapLocationResult: Sendable {
|
||||
case failure
|
||||
}
|
||||
|
||||
private struct MockRiskMapLocationProvider: LocationProviding {
|
||||
private final class MockRiskMapLocationProvider: LocationProviding, @unchecked Sendable {
|
||||
private let result: MockRiskMapLocationResult
|
||||
private let onCoordinateRequest: (@Sendable (CLLocationAccuracy) async -> Void)?
|
||||
private let lock = NSLock()
|
||||
private var recordedAccuracies: [CLLocationAccuracy] = []
|
||||
|
||||
init(_ result: MockRiskMapLocationResult) {
|
||||
var requestedAccuracies: [CLLocationAccuracy] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return recordedAccuracies
|
||||
}
|
||||
|
||||
init(
|
||||
_ result: MockRiskMapLocationResult,
|
||||
onCoordinateRequest: (@Sendable (CLLocationAccuracy) async -> Void)? = nil
|
||||
) {
|
||||
self.result = result
|
||||
self.onCoordinateRequest = onCoordinateRequest
|
||||
}
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
@ -1034,7 +1084,11 @@ private struct MockRiskMapLocationProvider: LocationProviding {
|
||||
}
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||||
lock.lock()
|
||||
recordedAccuracies.append(desiredAccuracy)
|
||||
lock.unlock()
|
||||
await onCoordinateRequest?(desiredAccuracy)
|
||||
switch result {
|
||||
case .success(let latitude, let longitude):
|
||||
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
|
||||
Reference in New Issue
Block a user