重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
1845 lines
79 KiB
Swift
1845 lines
79 KiB
Swift
//
|
||
// WildPhotographerReportTests.swift
|
||
// suixinkanTests
|
||
//
|
||
|
||
import CoreLocation
|
||
import MapKit
|
||
import XCTest
|
||
@testable import suixinkan
|
||
|
||
@MainActor
|
||
/// 举报摄影师模块测试,覆盖接口类型、提交校验、列表筛选和补充证据。
|
||
final class WildPhotographerReportTests: XCTestCase {
|
||
func testReportTypesAPILoadsRemoteOptions() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"list": [
|
||
{ "value": 1, "label": "私下收款" },
|
||
{ "value": 2, "label": "疑似打野" },
|
||
{ "value": 3, "label": "未戴工牌" },
|
||
{ "value": 4, "label": "其他" }
|
||
]
|
||
},
|
||
"time": "2026-07-08 14:21:21"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.reportTypes()
|
||
|
||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/types")
|
||
XCTAssertEqual(response.list, [
|
||
WildReportType(value: 1, label: "私下收款"),
|
||
WildReportType(value: 2, label: "疑似打野"),
|
||
WildReportType(value: 3, label: "未戴工牌"),
|
||
WildReportType(value: 4, label: "其他")
|
||
])
|
||
}
|
||
|
||
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("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"list": [
|
||
{
|
||
"id": 32,
|
||
"complaint_no": "JB20260708917",
|
||
"title": "私下收款举报",
|
||
"report_type": 1,
|
||
"report_type_text": "私下收款",
|
||
"desc": "我在",
|
||
"scenic_id": 128,
|
||
"scenic_name": "伊犁那拉提景区-5A",
|
||
"location_name": "伊犁那拉提景区-5A",
|
||
"latitude": "32.4299428",
|
||
"longitude": "119.4403263",
|
||
"handle_status": 0,
|
||
"handle_status_text": "待处理",
|
||
"created_at": "2026-07-08 16:06:59"
|
||
}
|
||
],
|
||
"total": 1,
|
||
"page": 1,
|
||
"page_size": 10
|
||
},
|
||
"time": "2026-07-08 16:07:30"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.reportList(WildReportListRequest(scenicId: 128, handleStatus: 0, page: 1, pageSize: 10))
|
||
|
||
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
|
||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/list")
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "scenic_id", value: "128")) == true)
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "handle_status", value: "0")) == true)
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page", value: "1")) == true)
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "page_size", value: "10")) == true)
|
||
XCTAssertEqual(response.total, 1)
|
||
XCTAssertEqual(response.page, 1)
|
||
XCTAssertEqual(response.pageSize, 10)
|
||
XCTAssertEqual(response.list.first?.complaintNo, "JB20260708917")
|
||
}
|
||
|
||
func testSubmitReportAPIReturnsComplaintNumber() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"data": {
|
||
"complaint_no": "JB20260709886",
|
||
"id": 40
|
||
},
|
||
"msg": "success",
|
||
"time": "2026-07-09 14:09:41"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.submitReport(WildReportSubmitRequest(
|
||
scenicId: 128,
|
||
reportType: 2,
|
||
desc: "测试说明",
|
||
storeUserId: nil,
|
||
contactPhone: "13800138000",
|
||
locationName: "测试景区",
|
||
locationAddress: "测试地址",
|
||
lat: 30.12,
|
||
lng: 120.34,
|
||
evidences: [WildReportEvidenceRequest(fileURL: "https://example.com/scene.jpg", fileType: 1, fileName: "scene.jpg")],
|
||
paymentScreenshots: []
|
||
))
|
||
|
||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/submit")
|
||
XCTAssertEqual(response.id, 40)
|
||
XCTAssertEqual(response.complaintNo, "JB20260709886")
|
||
}
|
||
|
||
func testReportDetailAPILoadsRemoteDetail() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"id": 32,
|
||
"complaint_no": "JB20260708917",
|
||
"title": "私下收款举报",
|
||
"report_type": 1,
|
||
"report_type_text": "私下收款",
|
||
"desc": "我在观景台附近看到私下收款",
|
||
"supplements": [],
|
||
"report_contents": [
|
||
{
|
||
"type": "initial",
|
||
"id": 0,
|
||
"desc": "我在观景台附近看到私下收款",
|
||
"time": "2026-07-08 16:06:59",
|
||
"time_label": "16:06:59",
|
||
"evidences": [
|
||
{
|
||
"file_type": 1,
|
||
"file_type_text": "图片",
|
||
"file_url": "https://example.com/report.jpg",
|
||
"file_name": "report.jpg"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"scenic_id": 128,
|
||
"scenic_name": "伊犁那拉提景区-5A",
|
||
"store_user_id": 0,
|
||
"contact_phone": "13800138000",
|
||
"location_name": "伊犁那拉提景区-5A",
|
||
"location_address": "江苏省扬州市邗江区瘦西湖路",
|
||
"latitude": "32.4299428",
|
||
"longitude": "119.4403263",
|
||
"handle_status": 0,
|
||
"handle_status_text": "待处理",
|
||
"handle_remark": "",
|
||
"handled_at": "2026-07-09 14:18:40",
|
||
"handler": {
|
||
"id": 43,
|
||
"name": "管铜彬",
|
||
"phone": "132****9413",
|
||
"started_at": "2026-07-09 14:18:15",
|
||
"finished_at": "2026-07-09 14:18:40",
|
||
"remark": "我是处理意见"
|
||
},
|
||
"handler_name": "管铜彬",
|
||
"handler_phone": "132****9413",
|
||
"evidence_summary": { "image_count": 1, "video_count": 1 },
|
||
"evidences": [
|
||
{
|
||
"file_type": 1,
|
||
"file_type_text": "图片",
|
||
"file_url": "https://example.com/report.jpg",
|
||
"file_name": "report.jpg"
|
||
},
|
||
{
|
||
"file_type": 2,
|
||
"file_type_text": "视频",
|
||
"file_url": "https://example.com/report.mov",
|
||
"file_name": "report.mov"
|
||
}
|
||
],
|
||
"handle_evidences": [
|
||
{
|
||
"evidence_kind": 2,
|
||
"evidence_kind_text": "处理凭证",
|
||
"file_type": 1,
|
||
"file_type_text": "图片",
|
||
"file_url": "http://tmp/handler-one.jpg",
|
||
"file_name": "handler-one.jpg"
|
||
},
|
||
{
|
||
"evidence_kind": 2,
|
||
"evidence_kind_text": "处理凭证",
|
||
"file_type": 1,
|
||
"file_type_text": "图片",
|
||
"file_url": "http://tmp/handler-two.png",
|
||
"file_name": "handler-two.png"
|
||
}
|
||
],
|
||
"payment_screenshots": [
|
||
{
|
||
"file_type": 5,
|
||
"file_type_text": "支付截图",
|
||
"file_url": "https://example.com/payment.jpg",
|
||
"file_name": "payment.jpg"
|
||
}
|
||
],
|
||
"created_at": "2026-07-08 16:06:59",
|
||
"timeline": [
|
||
{ "time": "2026-07-08 16:06:59", "content": "摄影师提交举报" }
|
||
]
|
||
},
|
||
"time": "2026-07-08 16:31:50"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.reportDetail(id: 32)
|
||
|
||
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
|
||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/detail")
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
|
||
XCTAssertEqual(response.complaintNo, "JB20260708917")
|
||
XCTAssertEqual(response.evidences.count, 2)
|
||
XCTAssertEqual(response.paymentScreenshots.first?.fileName, "payment.jpg")
|
||
XCTAssertEqual(response.timeline.first?.content, "摄影师提交举报")
|
||
XCTAssertEqual(response.handler?.name, "管铜彬")
|
||
XCTAssertEqual(response.handler?.phone, "132****9413")
|
||
XCTAssertEqual(response.handler?.startedAt, "2026-07-09 14:18:15")
|
||
XCTAssertEqual(response.handler?.finishedAt, "2026-07-09 14:18:40")
|
||
XCTAssertEqual(response.handler?.remark, "我是处理意见")
|
||
XCTAssertEqual(response.handledAt, "2026-07-09 14:18:40")
|
||
XCTAssertEqual(response.handleEvidences.count, 2)
|
||
XCTAssertEqual(response.handleEvidences.first?.evidenceKind, 2)
|
||
XCTAssertEqual(response.handleEvidences.first?.evidenceKindText, "处理凭证")
|
||
XCTAssertEqual(response.handleEvidences.first?.fileURL, "http://tmp/handler-one.jpg")
|
||
XCTAssertEqual(response.handleEvidences.last?.fileName, "handler-two.png")
|
||
}
|
||
|
||
func testReportShareAPILoadsMiniProgramConfig() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"userName": "wx8c85189bf3bdda29",
|
||
"path": "pages/scenic/radar/report-detail/index?id=32",
|
||
"title": "私下收款举报",
|
||
"imagePath": "/assets/share/report-share-cover.png",
|
||
"webpageUrl": "https://www.youfuntour.com",
|
||
"withShareTicket": true,
|
||
"miniprogramType": 0,
|
||
"scene": 0
|
||
},
|
||
"time": "2026-07-09 15:42:19"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.reportShare(id: "32")
|
||
|
||
let components = URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)
|
||
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/report/share")
|
||
XCTAssertTrue(components?.queryItems?.contains(URLQueryItem(name: "id", value: "32")) == true)
|
||
XCTAssertEqual(response.userName, "wx8c85189bf3bdda29")
|
||
XCTAssertEqual(response.path, "pages/scenic/radar/report-detail/index?id=32")
|
||
XCTAssertEqual(response.title, "私下收款举报")
|
||
XCTAssertEqual(response.imagePath, "/assets/share/report-share-cover.png")
|
||
XCTAssertEqual(response.webpageURL, "https://www.youfuntour.com")
|
||
XCTAssertTrue(response.withShareTicket)
|
||
XCTAssertEqual(response.miniProgramType, .release)
|
||
XCTAssertEqual(response.scene, .session)
|
||
XCTAssertEqual(response.payload.userName, "wx8c85189bf3bdda29")
|
||
XCTAssertEqual(response.payload.path, "pages/scenic/radar/report-detail/index?id=32")
|
||
}
|
||
|
||
func testSupplementReportAPISendsRequestBody() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": null,
|
||
"time": "2026-07-08 17:31:50"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
try await api.supplementReport(WildReportSupplementRequest(
|
||
id: 32,
|
||
desc: "补充说明",
|
||
evidences: [
|
||
WildReportEvidenceRequest(
|
||
fileURL: "https://example.com/supplement.jpg",
|
||
fileType: 1,
|
||
fileName: "supplement.jpg"
|
||
)
|
||
]
|
||
))
|
||
|
||
let request = try XCTUnwrap(session.requests.first)
|
||
XCTAssertEqual(request.httpMethod, "POST")
|
||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/supplement")
|
||
let bodyData = try XCTUnwrap(request.httpBody)
|
||
let body = try XCTUnwrap(JSONSerialization.jsonObject(with: bodyData) as? [String: Any])
|
||
XCTAssertEqual(body["id"] as? Int, 32)
|
||
XCTAssertEqual(body["desc"] as? String, "补充说明")
|
||
let evidences = try XCTUnwrap(body["evidences"] as? [[String: Any]])
|
||
XCTAssertEqual(evidences.first?["file_url"] as? String, "https://example.com/supplement.jpg")
|
||
XCTAssertEqual(evidences.first?["file_type"] as? Int, 1)
|
||
XCTAssertEqual(evidences.first?["file_name"] as? String, "supplement.jpg")
|
||
}
|
||
|
||
func testRiskMapAPISendsGETQueryAndDecodesMarkers() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"scenic_id": 128,
|
||
"scenic_name": "伊犁那拉提景区-5A",
|
||
"legend": [
|
||
{ "marker_type": "red", "title": "疑似打野" }
|
||
],
|
||
"green_marker_tip": "绿色为景区内摄影师",
|
||
"markers": [
|
||
{
|
||
"marker_type": "red",
|
||
"id": 32,
|
||
"latitude": "43.4068",
|
||
"longitude": "84.0582",
|
||
"location_name": "空中草原观景道",
|
||
"title": "线索 ID:32",
|
||
"report_type_text": "疑似打野",
|
||
"handle_status": 0,
|
||
"distance_m": 320,
|
||
"created_at": "2026-07-08 16:06:59"
|
||
},
|
||
{
|
||
"marker_type": "yellow",
|
||
"id": 33,
|
||
"latitude": "43.3652",
|
||
"longitude": "84.0385",
|
||
"location_name": "塔吾萨尼步道",
|
||
"title": "线索 ID:33",
|
||
"report_type_text": "处理中",
|
||
"handle_status": "1",
|
||
"distance_m": 1280,
|
||
"created_at": "2026-07-08 16:08:59"
|
||
}
|
||
]
|
||
},
|
||
"time": "2026-07-08 18:31:50"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.riskMap(WildReportRiskMapRequest(
|
||
scenicId: 128,
|
||
latitude: 43.3779,
|
||
longitude: 84.0217
|
||
))
|
||
|
||
let request = try XCTUnwrap(session.requests.first)
|
||
XCTAssertEqual(request.httpMethod, "GET")
|
||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/risk-map")
|
||
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"])
|
||
XCTAssertEqual(response.markers.last?.handleStatus?.displayText, WildReportStatus.processing.rawValue)
|
||
}
|
||
|
||
func testMarkerDetailAPISendsGETQueryAndDecodesGreenDetail() async throws {
|
||
let json = Data("""
|
||
{
|
||
"code": 100000,
|
||
"msg": "success",
|
||
"data": {
|
||
"marker_type": "green",
|
||
"id": 901,
|
||
"title": "景区内摄影师",
|
||
"tag": "认证",
|
||
"name": "阿依达娜",
|
||
"avatar": "https://example.com/avatar.jpg",
|
||
"store_name": "空中草原旅拍店",
|
||
"online": true,
|
||
"location_name": "空中草原",
|
||
"latitude": "43.4042",
|
||
"longitude": "84.0186",
|
||
"distance_m": 430,
|
||
"green_marker_tip": "绿色为景区内摄影师",
|
||
"activity_text": "当前在空中草原区域服务游客拍摄。"
|
||
},
|
||
"time": "2026-07-08 18:31:50"
|
||
}
|
||
""".utf8)
|
||
let session = MockURLSession(responses: [json])
|
||
let api = WildPhotographerReportAPI(client: APIClient(environment: .testing, session: session))
|
||
|
||
let response = try await api.markerDetail(WildReportMarkerDetailRequest(
|
||
scenicId: 128,
|
||
markerType: "green",
|
||
id: 901,
|
||
latitude: 43.3779,
|
||
longitude: 84.0217
|
||
))
|
||
|
||
let request = try XCTUnwrap(session.requests.first)
|
||
XCTAssertEqual(request.httpMethod, "GET")
|
||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/report/marker-detail")
|
||
XCTAssertNil(request.httpBody)
|
||
let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false))
|
||
let queryItems = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).map { ($0.name, $0.value) })
|
||
XCTAssertEqual(queryItems["scenic_id"], "128")
|
||
XCTAssertEqual(queryItems["marker_type"], "green")
|
||
XCTAssertEqual(queryItems["id"], "901")
|
||
XCTAssertEqual(queryItems["latitude"], "43.3779")
|
||
XCTAssertEqual(queryItems["longitude"], "84.0217")
|
||
XCTAssertEqual(response.name, "阿依达娜")
|
||
XCTAssertEqual(response.storeName, "空中草原旅拍店")
|
||
XCTAssertTrue(response.online)
|
||
}
|
||
|
||
func testReportListItemMapsToRecord() {
|
||
let item = makeReportListItem(
|
||
complaintNo: "JB20260708917",
|
||
title: "私下收款举报",
|
||
reportType: 1,
|
||
reportTypeText: "私下收款",
|
||
handleStatus: 0,
|
||
handleStatusText: "待处理"
|
||
)
|
||
|
||
let record = item.toRecord()
|
||
|
||
XCTAssertEqual(record.id, "JB20260708917")
|
||
XCTAssertEqual(record.title, "私下收款举报")
|
||
XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
|
||
XCTAssertEqual(record.status, .pending)
|
||
XCTAssertEqual(record.location, "伊犁那拉提景区-5A")
|
||
XCTAssertEqual(record.description, "测试说明")
|
||
XCTAssertEqual(record.submitTime, "2026-07-08 16:06:59")
|
||
XCTAssertEqual(record.imageCount, 0)
|
||
XCTAssertEqual(record.videoCount, 0)
|
||
XCTAssertEqual(record.serverID, 32)
|
||
}
|
||
|
||
func testReportDetailMapsToRecordAndSupplementData() {
|
||
let detail = makeReportDetailData(handleStatus: 3, handleStatusText: "驳回")
|
||
|
||
let record = detail.toRecord()
|
||
|
||
XCTAssertEqual(record.serverID, 32)
|
||
XCTAssertEqual(record.id, "JB20260708917")
|
||
XCTAssertEqual(record.reportType, WildReportType(value: 1, label: "私下收款"))
|
||
XCTAssertEqual(record.status, .rejected)
|
||
XCTAssertEqual(record.location, "伊犁那拉提景区-5A · 江苏省扬州市邗江区瘦西湖路")
|
||
XCTAssertEqual(record.description, "正式详情说明")
|
||
XCTAssertEqual(record.phoneNumber, "13800138000")
|
||
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() {
|
||
let titles = (0..<3).map {
|
||
WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: $0)
|
||
}
|
||
|
||
XCTAssertEqual(titles, ["第1次补充", "第2次补充", "第3次补充"])
|
||
}
|
||
|
||
func testReportDetailMapsHandlerInfoFromHandlerFields() throws {
|
||
let detail = makeReportDetailData(
|
||
handleStatus: 2,
|
||
handleStatusText: "已处理",
|
||
handleRemark: "顶层处理备注",
|
||
handledAt: "2026-07-09 14:18:40",
|
||
handler: WildReportDetailHandler(
|
||
id: 43,
|
||
name: "管铜彬",
|
||
phone: "132****9413",
|
||
startedAt: "2026-07-09 14:18:15",
|
||
finishedAt: "2026-07-09 14:18:40",
|
||
remark: "我是处理意见"
|
||
),
|
||
handleEvidences: [
|
||
WildReportDetailEvidence(
|
||
fileType: 1,
|
||
fileTypeText: "图片",
|
||
fileURL: "http://tmp/handler-one.jpg",
|
||
fileName: "handler-one.jpg",
|
||
evidenceKind: 2,
|
||
evidenceKindText: "处理凭证"
|
||
),
|
||
WildReportDetailEvidence(
|
||
fileType: 1,
|
||
fileTypeText: "图片",
|
||
fileURL: "http://tmp/handler-two.png",
|
||
fileName: "handler-two.png",
|
||
evidenceKind: 2,
|
||
evidenceKindText: "处理凭证"
|
||
)
|
||
]
|
||
)
|
||
|
||
let info = try XCTUnwrap(detail.toRecord().handlerInfo)
|
||
|
||
XCTAssertEqual(info.handlerName, "管铜彬")
|
||
XCTAssertEqual(info.handlerPhone, "132****9413")
|
||
XCTAssertEqual(info.processingAt, "2026-07-09 14:18:15")
|
||
XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
|
||
XCTAssertEqual(info.processOpinion, "我是处理意见")
|
||
XCTAssertEqual(info.completionMedia.count, 2)
|
||
XCTAssertEqual(info.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
|
||
XCTAssertEqual(info.completionMediaSummary, "2张")
|
||
}
|
||
|
||
func testReportDetailMapsHandlerInfoFromFallbackFields() throws {
|
||
let detail = makeReportDetailData(
|
||
handleStatus: 2,
|
||
handleStatusText: "已处理",
|
||
handleRemark: "fallback 处理意见",
|
||
handledAt: "2026-07-09 14:18:40",
|
||
handlerName: "管铜彬",
|
||
handlerPhone: "132****9413"
|
||
)
|
||
|
||
let info = try XCTUnwrap(detail.toRecord().handlerInfo)
|
||
|
||
XCTAssertEqual(info.handlerName, "管铜彬")
|
||
XCTAssertEqual(info.handlerPhone, "132****9413")
|
||
XCTAssertEqual(info.processingAt, "")
|
||
XCTAssertEqual(info.processedAt, "2026-07-09 14:18:40")
|
||
XCTAssertEqual(info.processOpinion, "fallback 处理意见")
|
||
}
|
||
|
||
func testReportShareDataDecodesSnakeCaseAndMapsPayload() throws {
|
||
let json = Data("""
|
||
{
|
||
"user_name": "gh_report_original_id",
|
||
"path": "pages/report/detail?id=32",
|
||
"title": "私下收款举报",
|
||
"description": "查看举报处理进度",
|
||
"image_path": "https://example.com/share.png",
|
||
"webpage_url": "https://www.youfuntour.com/report/32",
|
||
"with_share_ticket": true,
|
||
"mini_program_type": 2,
|
||
"scene": 0,
|
||
"disable_forward": true
|
||
}
|
||
""".utf8)
|
||
|
||
let data = try JSONDecoder().decode(WildReportMiniProgramShareData.self, from: json)
|
||
|
||
XCTAssertEqual(data.userName, "gh_report_original_id")
|
||
XCTAssertEqual(data.webpageURL, "https://www.youfuntour.com/report/32")
|
||
XCTAssertEqual(data.miniProgramType, .preview)
|
||
XCTAssertEqual(data.scene, .session)
|
||
XCTAssertEqual(data.payload.description, "查看举报处理进度")
|
||
XCTAssertTrue(data.payload.withShareTicket)
|
||
XCTAssertTrue(data.payload.disableForward)
|
||
}
|
||
|
||
func testReportStatusMapsRejected() {
|
||
XCTAssertEqual(WildReportStatus(handleStatus: 0), .pending)
|
||
XCTAssertEqual(WildReportStatus(handleStatus: 1), .processing)
|
||
XCTAssertEqual(WildReportStatus(handleStatus: 2), .processed)
|
||
XCTAssertEqual(WildReportStatus(handleStatus: 3), .rejected)
|
||
}
|
||
|
||
func testReportListAllFilterOmitsHandleStatus() {
|
||
let request = WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10)
|
||
|
||
XCTAssertFalse(request.queryItems.contains { $0.name == "handle_status" })
|
||
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "scenic_id", value: "128")))
|
||
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page", value: "1")))
|
||
XCTAssertTrue(request.queryItems.contains(URLQueryItem(name: "page_size", value: "10")))
|
||
}
|
||
|
||
func testSubmitViewModelLoadsReportTypesFromAPI() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportTypeResponse = WildReportTypeListResponse(list: [
|
||
WildReportType(value: 1, label: "私下收款"),
|
||
WildReportType(value: 2, label: "疑似打野"),
|
||
WildReportType(value: 4, label: "其他"),
|
||
])
|
||
|
||
await viewModel.loadReportTypes(api: api)
|
||
|
||
XCTAssertEqual(viewModel.reportTypes, [
|
||
WildReportType(value: 1, label: "私下收款"),
|
||
WildReportType(value: 2, label: "疑似打野"),
|
||
WildReportType(value: 4, label: "其他")
|
||
])
|
||
XCTAssertEqual(viewModel.selectedReportType, WildReportType(value: 1, label: "私下收款"))
|
||
}
|
||
|
||
func testSubmitViewModelKeepsReportTypesEmptyWhenReportTypesFail() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.error = URLError(.badServerResponse)
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
await viewModel.loadReportTypes(api: api)
|
||
|
||
XCTAssertTrue(viewModel.reportTypes.isEmpty)
|
||
XCTAssertNil(viewModel.selectedReportType)
|
||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||
}
|
||
|
||
func testSubmitLocationOmitsProvinceCityDistrict() {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
|
||
viewModel.applyResolvedLocation(HomeLocationSnapshot(
|
||
latitude: 32.40,
|
||
longitude: 119.41,
|
||
address: "江苏省扬州市邗江区瘦西湖路"
|
||
))
|
||
|
||
XCTAssertEqual(viewModel.locationTitleText, "当前定位:瘦西湖路")
|
||
XCTAssertEqual(viewModel.reportLocation, "瘦西湖路")
|
||
}
|
||
|
||
func testSubmitTextInputUpdatesWithoutRequestingStructuralRefresh() {
|
||
let viewModel = WildPhotographerReportSubmitViewModel(
|
||
homeViewModel: WildPhotographerReportHomeViewModel()
|
||
)
|
||
var stateChangeCount = 0
|
||
viewModel.onStateChange = { stateChangeCount += 1 }
|
||
|
||
viewModel.updateDescription("现场说明")
|
||
viewModel.updateContact("wx-test")
|
||
|
||
XCTAssertEqual(viewModel.description, "现场说明")
|
||
XCTAssertEqual(viewModel.contact, "wx-test")
|
||
XCTAssertEqual(stateChangeCount, 0)
|
||
}
|
||
|
||
func testSubmitFailsWithoutEvidence() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||
viewModel.updateDescription("有说明")
|
||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))
|
||
|
||
await viewModel.submitReport(
|
||
api: MockWildPhotographerReportAPI(),
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 9,
|
||
scenicName: "测试景区"
|
||
)
|
||
|
||
XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
|
||
XCTAssertNil(viewModel.submitResult)
|
||
}
|
||
|
||
func testSubmitFailsWithoutDescription() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||
viewModel.updateDescription(" ")
|
||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试位置"))
|
||
|
||
await viewModel.submitReport(
|
||
api: MockWildPhotographerReportAPI(),
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 9,
|
||
scenicName: "测试景区"
|
||
)
|
||
|
||
XCTAssertEqual(viewModel.validationMessage, "请填写举报说明。")
|
||
XCTAssertNil(viewModel.submitResult)
|
||
}
|
||
|
||
func testSubmitFailsWithoutConfirmedLocation() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
viewModel.selectReportType(WildReportType(value: 2, label: "疑似打野"))
|
||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "scene.jpg")])
|
||
viewModel.updateDescription("测试说明")
|
||
|
||
await viewModel.submitReport(
|
||
api: MockWildPhotographerReportAPI(),
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 9,
|
||
scenicName: "测试景区"
|
||
)
|
||
|
||
XCTAssertEqual(viewModel.validationMessage, "请先更新举报位置后再提交举报。")
|
||
XCTAssertNil(viewModel.submitResult)
|
||
}
|
||
|
||
func testSubmitSuccessCreatesRecord() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.submitResponse = WildReportSubmitResponse(id: 40, complaintNo: "JB20260709886")
|
||
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("测试说明")
|
||
viewModel.applyResolvedLocation(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "江苏省扬州市邗江区瘦西湖路"))
|
||
|
||
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
|
||
|
||
XCTAssertNil(viewModel.validationMessage)
|
||
XCTAssertNotNil(viewModel.submitResult)
|
||
XCTAssertEqual(viewModel.submitResult?.record.serverID, 40)
|
||
XCTAssertEqual(viewModel.submitResult?.record.id, "JB20260709886")
|
||
XCTAssertEqual(uploader.uploadedFileNames, ["scene.jpg", "payment.jpg"])
|
||
XCTAssertEqual(api.submitRequests.first?.scenicId, 9)
|
||
XCTAssertEqual(api.submitRequests.first?.reportType, 2)
|
||
XCTAssertEqual(api.submitRequests.first?.lat, 30.12)
|
||
XCTAssertEqual(api.submitRequests.first?.lng, 120.34)
|
||
XCTAssertEqual(api.submitRequests.first?.locationName, "测试景区")
|
||
XCTAssertEqual(api.submitRequests.first?.locationAddress, "瘦西湖路")
|
||
XCTAssertEqual(api.submitRequests.first?.evidences.count, 1)
|
||
XCTAssertEqual(api.submitRequests.first?.evidences.first?.fileType, 1)
|
||
XCTAssertEqual(api.submitRequests.first?.paymentScreenshots.count, 1)
|
||
XCTAssertNil(api.submitRequests.first?.storeUserId)
|
||
XCTAssertEqual(viewModel.submitResult?.record.location, "瘦西湖路")
|
||
}
|
||
|
||
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(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
||
|
||
await viewModel.submitReport(
|
||
api: MockWildPhotographerReportAPI(),
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 9,
|
||
scenicName: "测试景区"
|
||
)
|
||
|
||
XCTAssertEqual(viewModel.validationMessage, "请至少上传一项现场证据。")
|
||
}
|
||
|
||
func testPaymentScreenshotsLimitedToThree() throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
|
||
viewModel.addPaymentImages([
|
||
try makeAttachment(kind: .payment, fileName: "p1.jpg"),
|
||
try makeAttachment(kind: .payment, fileName: "p2.jpg"),
|
||
try makeAttachment(kind: .payment, fileName: "p3.jpg"),
|
||
try makeAttachment(kind: .payment, fileName: "p4.jpg")
|
||
])
|
||
|
||
XCTAssertEqual(viewModel.paymentImages.count, 3)
|
||
}
|
||
|
||
func testUploadFailureSkipsSubmitAPI() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportSubmitViewModel(homeViewModel: homeViewModel)
|
||
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(HomeLocationSnapshot(latitude: 30.12, longitude: 120.34, address: "测试景区 · 测试点"))
|
||
|
||
await viewModel.submitReport(api: api, uploader: uploader, scenicId: 9, scenicName: "测试景区")
|
||
|
||
XCTAssertEqual(api.submitRequests.count, 0)
|
||
XCTAssertEqual(viewModel.validationMessage, OSSUploadError.emptyFile.localizedDescription)
|
||
}
|
||
|
||
func testListInitialLoadUsesAllStatusAndFirstPage() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportListResponses = [
|
||
WildReportListResponse(
|
||
list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)],
|
||
total: 1,
|
||
page: 1,
|
||
pageSize: 10
|
||
)
|
||
]
|
||
|
||
await viewModel.loadInitial(api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(api.reportListRequests.first, WildReportListRequest(scenicId: 128, handleStatus: nil, page: 1, pageSize: 10))
|
||
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
|
||
XCTAssertFalse(viewModel.hasMore)
|
||
}
|
||
|
||
func testListFilterReloadsWithMappedStatus() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportListResponses = [
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 1, page: 1, pageSize: 10),
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 1)], total: 1, page: 1, pageSize: 10),
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB003", handleStatus: 2)], total: 1, page: 1, pageSize: 10),
|
||
]
|
||
|
||
await viewModel.selectFilter(.pending, api: api, scenicId: 128)
|
||
await viewModel.selectFilter(.processing, api: api, scenicId: 128)
|
||
await viewModel.selectFilter(.processed, api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(api.reportListRequests.map(\.handleStatus), [0, 1, 2] as [Int?])
|
||
XCTAssertEqual(api.reportListRequests.map(\.page), [1, 1, 1])
|
||
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB003"])
|
||
}
|
||
|
||
func testListLoadMoreAppendsReportsAndUpdatesHasMore() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportListResponses = [
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10),
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB002", handleStatus: 0)], total: 2, page: 2, pageSize: 10),
|
||
]
|
||
|
||
await viewModel.loadInitial(api: api, scenicId: 128)
|
||
XCTAssertTrue(viewModel.hasMore)
|
||
await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(api.reportListRequests.map(\.page), [1, 2])
|
||
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001", "JB002"])
|
||
XCTAssertFalse(viewModel.hasMore)
|
||
}
|
||
|
||
func testListLoadFailureKeepsExistingReports() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportListResponses = [
|
||
WildReportListResponse(list: [makeReportListItem(complaintNo: "JB001", handleStatus: 0)], total: 2, page: 1, pageSize: 10)
|
||
]
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
await viewModel.loadInitial(api: api, scenicId: 128)
|
||
api.reportListError = URLError(.badServerResponse)
|
||
await viewModel.loadMoreIfNeeded(api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(viewModel.filteredReports.map(\.id), ["JB001"])
|
||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||
}
|
||
|
||
func testListViewModelLoadsMiniProgramSharePayload() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportShareResponse = WildReportMiniProgramShareData(
|
||
userName: "gh_report_original_id",
|
||
path: "pages/report/detail?id=32",
|
||
title: "私下收款举报",
|
||
description: "查看举报处理进度",
|
||
webpageURL: "https://www.youfuntour.com/report/32",
|
||
withShareTicket: true,
|
||
miniProgramTypeValue: WeChatMiniProgramType.preview.rawValue
|
||
)
|
||
|
||
let maybePayload = await viewModel.shareReport(record, api: api)
|
||
let payload = try XCTUnwrap(maybePayload)
|
||
|
||
XCTAssertEqual(api.reportShareRequests, ["32"])
|
||
XCTAssertEqual(payload.title, "私下收款举报")
|
||
XCTAssertEqual(payload.userName, "gh_report_original_id")
|
||
XCTAssertEqual(payload.path, "pages/report/detail?id=32")
|
||
XCTAssertEqual(payload.miniProgramType, .preview)
|
||
XCTAssertTrue(payload.withShareTicket)
|
||
}
|
||
|
||
func testListViewModelShareRejectsLocalRecord() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
|
||
let record = WildReportRecord(
|
||
serverID: nil,
|
||
id: "JBLOCAL",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||
status: .pending,
|
||
location: "测试景区",
|
||
submitTime: "2026-07-08 16:06:59",
|
||
description: "本地记录",
|
||
wechatID: "",
|
||
phoneNumber: "",
|
||
imageCount: 1,
|
||
videoCount: 0,
|
||
handlerInfo: nil
|
||
)
|
||
let api = MockWildPhotographerReportAPI()
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
let payload = await viewModel.shareReport(record, api: api)
|
||
|
||
XCTAssertNil(payload)
|
||
XCTAssertTrue(api.reportShareRequests.isEmpty)
|
||
XCTAssertEqual(messages.last, "当前举报暂未同步到服务器,无法分享")
|
||
}
|
||
|
||
func testSupplementEvidenceValidationAndSuccess() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
let uploader = MockWildReportAttachmentUploader()
|
||
|
||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||
XCTAssertEqual(viewModel.validationMessage, "请至少填写补充说明或上传一项证据。")
|
||
|
||
viewModel.updateText("补充说明")
|
||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||
viewModel.addVideos([try makeAttachment(kind: .video, fileName: "supplement.mov")])
|
||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||
|
||
XCTAssertTrue(viewModel.didSubmit)
|
||
XCTAssertEqual(uploader.uploadedFileNames, ["supplement.jpg", "supplement.mov"])
|
||
XCTAssertEqual(api.supplementRequests.first?.id, 32)
|
||
XCTAssertEqual(api.supplementRequests.first?.desc, "补充说明")
|
||
XCTAssertEqual(api.supplementRequests.first?.evidences?.map(\.fileType), [1, 2])
|
||
}
|
||
|
||
func testSupplementEvidenceFailsWithoutServerID() async {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let record = WildReportRecord(
|
||
serverID: nil,
|
||
id: "JBLOCAL",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||
status: .pending,
|
||
location: "测试景区",
|
||
submitTime: "2026-07-08 16:06:59",
|
||
description: "本地记录",
|
||
wechatID: "",
|
||
phoneNumber: "",
|
||
imageCount: 1,
|
||
videoCount: 0,
|
||
handlerInfo: nil
|
||
)
|
||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||
viewModel.updateText("补充说明")
|
||
|
||
await viewModel.submit(
|
||
api: MockWildPhotographerReportAPI(),
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 128
|
||
)
|
||
|
||
XCTAssertEqual(viewModel.validationMessage, "当前举报暂未同步到服务器,无法补充证据")
|
||
XCTAssertFalse(viewModel.didSubmit)
|
||
}
|
||
|
||
func testSupplementEvidenceTextAndAttachmentLimits() throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||
|
||
viewModel.updateText(String(repeating: "测", count: 520))
|
||
XCTAssertEqual(viewModel.text.count, 500)
|
||
|
||
let images = try (0..<10).map { try makeAttachment(kind: .image, fileName: "image\($0).jpg") }
|
||
viewModel.addImages(images)
|
||
XCTAssertEqual(viewModel.images.count, 9)
|
||
|
||
viewModel.removeAttachment(viewModel.images[0])
|
||
let videos = try (0..<4).map { try makeAttachment(kind: .video, fileName: "video\($0).mov") }
|
||
viewModel.addVideos(videos)
|
||
XCTAssertEqual(viewModel.videos.count, 1)
|
||
}
|
||
|
||
func testSupplementEvidenceUploadFailureDoesNotSubmit() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
let uploader = MockWildReportAttachmentUploader()
|
||
uploader.error = URLError(.cannotWriteToFile)
|
||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||
|
||
await viewModel.submit(api: api, uploader: uploader, scenicId: 128)
|
||
|
||
XCTAssertFalse(viewModel.didSubmit)
|
||
XCTAssertTrue(api.supplementRequests.isEmpty)
|
||
XCTAssertEqual(viewModel.validationMessage, URLError(.cannotWriteToFile).localizedDescription)
|
||
}
|
||
|
||
func testSupplementEvidenceAPIErrorDoesNotSucceed() async throws {
|
||
let homeViewModel = WildPhotographerReportHomeViewModel()
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.supplementError = URLError(.badServerResponse)
|
||
viewModel.addImages([try makeAttachment(kind: .image, fileName: "supplement.jpg")])
|
||
|
||
await viewModel.submit(
|
||
api: api,
|
||
uploader: MockWildReportAttachmentUploader(),
|
||
scenicId: 128
|
||
)
|
||
|
||
XCTAssertFalse(viewModel.didSubmit)
|
||
XCTAssertEqual(api.supplementRequests.count, 0)
|
||
XCTAssertEqual(viewModel.validationMessage, URLError(.badServerResponse).localizedDescription)
|
||
}
|
||
|
||
func testRiskMapViewModelRejectsInvalidScenicID() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 0
|
||
)
|
||
|
||
XCTAssertTrue(api.riskMapRequests.isEmpty)
|
||
XCTAssertEqual(viewModel.errorMessage, "请先选择景区后再查看风险地图")
|
||
}
|
||
|
||
func testRiskMapViewModelLoadsWithLocationAndMapsMarkerTypes() async {
|
||
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: 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 })
|
||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .processingClue })
|
||
XCTAssertTrue(viewModel.markers.contains { $0.kind == .peerPhotographer })
|
||
XCTAssertEqual(viewModel.selectedMarker?.kind, .selfLocation)
|
||
let yellow = viewModel.markers.first { $0.kind == .processingClue }
|
||
XCTAssertEqual(yellow.flatMap { marker in
|
||
if case .activeClue(let clue) = marker.detail { return clue.distanceText }
|
||
return nil
|
||
}, "1.3公里")
|
||
}
|
||
|
||
func testRiskMapViewModelLoadsWithoutLocationWhenLocationFails() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.failure),
|
||
scenicId: 128
|
||
)
|
||
|
||
XCTAssertEqual(api.riskMapRequests.first?.scenicId, 128)
|
||
XCTAssertNil(api.riskMapRequests.first?.latitude)
|
||
XCTAssertNil(api.riskMapRequests.first?.longitude)
|
||
XCTAssertFalse(viewModel.markers.contains { $0.kind == .selfLocation })
|
||
}
|
||
|
||
func testRiskMapViewModelFocusCurrentLocationSelectsSelfMarker() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 128
|
||
)
|
||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||
await viewModel.selectMarker(redMarker)
|
||
|
||
let didFocus = viewModel.focusCurrentLocation()
|
||
|
||
XCTAssertTrue(didFocus)
|
||
XCTAssertEqual(viewModel.selectedMarker?.kind, .selfLocation)
|
||
XCTAssertEqual(viewModel.region.center.latitude, 43.3779, accuracy: 0.000001)
|
||
XCTAssertEqual(viewModel.region.center.longitude, 84.0217, accuracy: 0.000001)
|
||
XCTAssertTrue(api.markerDetailRequests.isEmpty)
|
||
}
|
||
|
||
func testRiskMapViewModelLoadsMarkerDetailAndUpdatesSelectedClue() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
api.markerDetailResponses = [
|
||
WildReportMarkerDetailResponse(
|
||
markerType: "red",
|
||
id: 32,
|
||
title: "线索 ID:32",
|
||
locationName: "空中草原观景道",
|
||
latitude: "43.4068",
|
||
longitude: "84.0582",
|
||
distanceM: 320,
|
||
reportTypeText: "疑似打野",
|
||
handleStatus: WildReportDecodedHandleStatus(intValue: 0),
|
||
desc: "多人结伴揽客",
|
||
reporterName: "张三",
|
||
reporterPhone: "13333333333",
|
||
createdAt: "2026-07-08 16:06:59"
|
||
)
|
||
]
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 128
|
||
)
|
||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||
|
||
await viewModel.selectMarker(redMarker, api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(api.markerDetailRequests.first?.markerType, "red")
|
||
XCTAssertEqual(api.markerDetailRequests.first?.id, 32)
|
||
XCTAssertEqual(viewModel.selectedClue?.reporterName, "张三")
|
||
XCTAssertEqual(viewModel.selectedClue?.maskedReporterPhone, "133****3333")
|
||
XCTAssertEqual(viewModel.selectedClue?.detail, "多人结伴揽客")
|
||
}
|
||
|
||
func testRiskMapViewModelMapsProcessingMarkerDetailForBottomCard() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
api.markerDetailResponses = [
|
||
WildReportMarkerDetailResponse(
|
||
markerType: "yellow",
|
||
id: 33,
|
||
clueId: "2026年7月9日-0039",
|
||
title: "疑似打野摄影师举报",
|
||
statusTag: "正在处理中",
|
||
latitude: "32.3774100",
|
||
longitude: "119.3979350",
|
||
distanceM: 7068,
|
||
reportTypeText: "疑似打野",
|
||
desc: "色地方噶是的法规",
|
||
reporterName: "游客",
|
||
reporterPhone: "121****1212",
|
||
createdAt: "2026-07-09 11:37:26",
|
||
evidences: [
|
||
WildReportDetailEvidence(fileType: 1, fileTypeText: "图片", fileURL: "https://example.com/one.jpg"),
|
||
WildReportDetailEvidence(fileType: 1, fileTypeText: "图片", fileURL: "https://example.com/two.jpg")
|
||
]
|
||
)
|
||
]
|
||
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 32.429943033854165, longitude: 119.44032552083333)),
|
||
scenicId: 128
|
||
)
|
||
let yellowMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .processingClue })
|
||
|
||
await viewModel.selectMarker(yellowMarker, api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(api.markerDetailRequests.first?.markerType, "yellow")
|
||
XCTAssertEqual(viewModel.selectedClue?.id, "2026年7月9日-0039")
|
||
XCTAssertEqual(viewModel.selectedClue?.statusText, "正在处理中")
|
||
XCTAssertEqual(viewModel.selectedClue?.reporterName, "游客")
|
||
XCTAssertEqual(viewModel.selectedClue?.reporterPhone, "121****1212")
|
||
XCTAssertEqual(viewModel.selectedClue?.reportContents.first?.content, "色地方噶是的法规")
|
||
XCTAssertEqual(viewModel.selectedClue?.images, ["https://example.com/one.jpg", "https://example.com/two.jpg"])
|
||
XCTAssertEqual(viewModel.selectedClue?.processingStarted, true)
|
||
}
|
||
|
||
func testRiskMapViewModelMarkerDetailFailureKeepsSummary() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
api.markerDetailError = URLError(.badServerResponse)
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 128
|
||
)
|
||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||
|
||
await viewModel.selectMarker(redMarker, api: api, scenicId: 128)
|
||
|
||
XCTAssertEqual(viewModel.selectedClue?.summary, "疑似打野")
|
||
XCTAssertEqual(viewModel.errorMessage, URLError(.badServerResponse).localizedDescription)
|
||
}
|
||
|
||
func testRiskMapViewModelSharesSelectedReportMarker() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
api.reportShareResponse = WildReportMiniProgramShareData(
|
||
userName: "gh_test",
|
||
path: "/pages/report/detail?id=32",
|
||
title: "举报线索",
|
||
description: "请协助处理",
|
||
webpageURL: "https://example.com/report/32",
|
||
miniProgramTypeValue: WeChatMiniProgramType.preview.rawValue
|
||
)
|
||
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 128
|
||
)
|
||
let redMarker = try! XCTUnwrap(viewModel.markers.first { $0.kind == .wildPhotographer })
|
||
await viewModel.selectMarker(redMarker)
|
||
|
||
let payload = await viewModel.shareSelectedClue(api: api)
|
||
|
||
XCTAssertEqual(api.reportShareRequests, ["32"])
|
||
XCTAssertEqual(payload?.title, "举报线索")
|
||
XCTAssertEqual(payload?.path, "/pages/report/detail?id=32")
|
||
XCTAssertEqual(payload?.miniProgramType, .preview)
|
||
}
|
||
|
||
func testRiskMapViewModelDoesNotShareCurrentLocationMarker() async {
|
||
let viewModel = WildReportRiskMapViewModel()
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.riskMapResponses = [makeRiskMapResponse()]
|
||
|
||
await viewModel.loadInitial(
|
||
api: api,
|
||
locationProvider: MockRiskMapLocationProvider(.success(latitude: 43.3779, longitude: 84.0217)),
|
||
scenicId: 128
|
||
)
|
||
|
||
let payload = await viewModel.shareSelectedClue(api: api)
|
||
|
||
XCTAssertNil(payload)
|
||
XCTAssertTrue(api.reportShareRequests.isEmpty)
|
||
XCTAssertEqual(viewModel.errorMessage, "请选择可分享的举报线索")
|
||
}
|
||
|
||
func testDetailViewModelLoadsRemoteDetailWhenServerIDExists() async {
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportDetailResponses = [
|
||
makeReportDetailData(
|
||
handleStatus: 2,
|
||
handleStatusText: "已处理",
|
||
handler: WildReportDetailHandler(
|
||
id: 43,
|
||
name: "管铜彬",
|
||
phone: "132****9413",
|
||
startedAt: "2026-07-09 14:18:15",
|
||
finishedAt: "2026-07-09 14:18:40",
|
||
remark: "我是处理意见"
|
||
),
|
||
handleEvidences: [
|
||
WildReportDetailEvidence(
|
||
fileType: 1,
|
||
fileTypeText: "图片",
|
||
fileURL: "http://tmp/handler-one.jpg",
|
||
fileName: "handler-one.jpg"
|
||
)
|
||
]
|
||
)
|
||
]
|
||
|
||
await viewModel.loadDetailIfNeeded(api: api)
|
||
|
||
XCTAssertEqual(api.reportDetailRequests, [32])
|
||
XCTAssertEqual(viewModel.record.description, "正式详情说明")
|
||
XCTAssertEqual(viewModel.reporterMediaItems.count, 2)
|
||
XCTAssertEqual(viewModel.paymentScreenshotItems.count, 1)
|
||
XCTAssertEqual(viewModel.progressSteps.first?.title, "摄影师提交举报")
|
||
XCTAssertTrue(viewModel.showHandlerInfo)
|
||
XCTAssertEqual(viewModel.handlerInfo?.handlerName, "管铜彬")
|
||
XCTAssertEqual(viewModel.handlerInfo?.processingAt, "2026-07-09 14:18:15")
|
||
XCTAssertEqual(viewModel.handlerInfo?.completionMedia.first?.coverURL, "http://tmp/handler-one.jpg")
|
||
}
|
||
|
||
func testDetailViewModelFailureKeepsInitialRecord() async {
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportDetailError = URLError(.badServerResponse)
|
||
var messages: [String] = []
|
||
viewModel.onShowMessage = { messages.append($0) }
|
||
|
||
await viewModel.loadDetailIfNeeded(api: api)
|
||
|
||
XCTAssertEqual(viewModel.record.description, "测试说明")
|
||
XCTAssertEqual(messages.last, URLError(.badServerResponse).localizedDescription)
|
||
}
|
||
|
||
func testDetailViewModelLoadsMiniProgramSharePayload() async throws {
|
||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||
let api = MockWildPhotographerReportAPI()
|
||
api.reportShareResponse = WildReportMiniProgramShareData(
|
||
userName: "gh_report_original_id",
|
||
path: "pages/report/detail?id=32",
|
||
title: "私下收款举报",
|
||
webpageURL: "https://www.youfuntour.com/report/32",
|
||
miniProgramTypeValue: WeChatMiniProgramType.test.rawValue
|
||
)
|
||
|
||
let maybePayload = await viewModel.shareReport(api: api)
|
||
let payload = try XCTUnwrap(maybePayload)
|
||
|
||
XCTAssertEqual(api.reportShareRequests, ["32"])
|
||
XCTAssertEqual(payload.webpageURL, "https://www.youfuntour.com/report/32")
|
||
XCTAssertEqual(payload.userName, "gh_report_original_id")
|
||
XCTAssertEqual(payload.miniProgramType, .test)
|
||
}
|
||
|
||
func testDetailViewModelWithoutServerIDSkipsRequest() async {
|
||
let record = WildReportRecord(
|
||
id: "JBLOCAL",
|
||
title: "摄影师举报",
|
||
reportType: WildReportType(value: 2, label: "疑似打野"),
|
||
status: .pending,
|
||
location: "测试景区",
|
||
submitTime: "2026-07-08 16:06:59",
|
||
description: "本地提交记录",
|
||
wechatID: "",
|
||
phoneNumber: "",
|
||
imageCount: 1,
|
||
videoCount: 0,
|
||
handlerInfo: nil
|
||
)
|
||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||
let api = MockWildPhotographerReportAPI()
|
||
|
||
await viewModel.loadDetailIfNeeded(api: api)
|
||
|
||
XCTAssertTrue(api.reportDetailRequests.isEmpty)
|
||
XCTAssertEqual(viewModel.record.description, "本地提交记录")
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
private final class MockWildPhotographerReportAPI: WildPhotographerReportServing {
|
||
var reportTypeResponse = WildReportTypeListResponse(list: [])
|
||
var reportCopyResponse = WildReportCopyResponse()
|
||
var reportCopyError: Error?
|
||
var error: Error?
|
||
var submitResponse = WildReportSubmitResponse()
|
||
var submitRequests: [WildReportSubmitRequest] = []
|
||
var reportListResponses: [WildReportListResponse] = []
|
||
var reportListRequests: [WildReportListRequest] = []
|
||
var reportListError: Error?
|
||
var reportDetailResponses: [WildReportDetailData] = []
|
||
var reportDetailRequests: [Int] = []
|
||
var reportDetailError: Error?
|
||
var reportShareResponse = WildReportMiniProgramShareData()
|
||
var reportShareRequests: [String] = []
|
||
var reportShareError: Error?
|
||
var supplementRequests: [WildReportSupplementRequest] = []
|
||
var supplementError: Error?
|
||
var riskMapResponses: [WildReportRiskMapResponse] = []
|
||
var riskMapRequests: [WildReportRiskMapRequest] = []
|
||
var riskMapError: Error?
|
||
var markerDetailResponses: [WildReportMarkerDetailResponse] = []
|
||
var markerDetailRequests: [WildReportMarkerDetailRequest] = []
|
||
var markerDetailError: Error?
|
||
|
||
func reportTypes() async throws -> WildReportTypeListResponse {
|
||
if let error { throw error }
|
||
return reportTypeResponse
|
||
}
|
||
|
||
func reportCopy() async throws -> WildReportCopyResponse {
|
||
if let reportCopyError { throw reportCopyError }
|
||
return reportCopyResponse
|
||
}
|
||
|
||
func submitReport(_ request: WildReportSubmitRequest) async throws -> WildReportSubmitResponse {
|
||
if let error { throw error }
|
||
submitRequests.append(request)
|
||
return submitResponse
|
||
}
|
||
|
||
func supplementReport(_ request: WildReportSupplementRequest) async throws {
|
||
if let supplementError { throw supplementError }
|
||
supplementRequests.append(request)
|
||
}
|
||
|
||
func riskMap(_ request: WildReportRiskMapRequest) async throws -> WildReportRiskMapResponse {
|
||
if let riskMapError { throw riskMapError }
|
||
riskMapRequests.append(request)
|
||
guard !riskMapResponses.isEmpty else {
|
||
return WildReportRiskMapResponse(scenicId: request.scenicId, scenicName: "测试景区")
|
||
}
|
||
return riskMapResponses.removeFirst()
|
||
}
|
||
|
||
func markerDetail(_ request: WildReportMarkerDetailRequest) async throws -> WildReportMarkerDetailResponse {
|
||
if let markerDetailError { throw markerDetailError }
|
||
markerDetailRequests.append(request)
|
||
guard !markerDetailResponses.isEmpty else {
|
||
return WildReportMarkerDetailResponse(markerType: request.markerType, id: request.id)
|
||
}
|
||
return markerDetailResponses.removeFirst()
|
||
}
|
||
|
||
func reportList(_ request: WildReportListRequest) async throws -> WildReportListResponse {
|
||
if let reportListError { throw reportListError }
|
||
reportListRequests.append(request)
|
||
guard !reportListResponses.isEmpty else {
|
||
return WildReportListResponse(list: [], total: 0, page: request.page, pageSize: request.pageSize)
|
||
}
|
||
return reportListResponses.removeFirst()
|
||
}
|
||
|
||
func reportDetail(id: Int) async throws -> WildReportDetailData {
|
||
if let reportDetailError { throw reportDetailError }
|
||
reportDetailRequests.append(id)
|
||
guard !reportDetailResponses.isEmpty else { return WildReportDetailData(id: id) }
|
||
return reportDetailResponses.removeFirst()
|
||
}
|
||
|
||
func reportShare(id: String) async throws -> WildReportMiniProgramShareData {
|
||
if let reportShareError { throw reportShareError }
|
||
reportShareRequests.append(id)
|
||
return reportShareResponse
|
||
}
|
||
}
|
||
|
||
private enum MockRiskMapLocationResult: Sendable {
|
||
case success(latitude: Double, longitude: Double)
|
||
case failure
|
||
}
|
||
|
||
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] = []
|
||
|
||
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(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
||
switch result {
|
||
case .success(let latitude, let longitude):
|
||
return HomeLocationSnapshot(latitude: latitude, longitude: longitude, address: "测试地址")
|
||
case .failure:
|
||
throw LocationProviderError.locationFailed
|
||
}
|
||
}
|
||
|
||
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)
|
||
case .failure:
|
||
throw LocationProviderError.locationFailed
|
||
}
|
||
}
|
||
|
||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||
"测试地址"
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
private final class MockWildReportAttachmentUploader: WildReportAttachmentUploading {
|
||
var uploadedFileNames: [String] = []
|
||
var error: Error?
|
||
|
||
func uploadWildReportAttachment(
|
||
data: Data,
|
||
fileName: String,
|
||
fileType: Int,
|
||
scenicId: Int,
|
||
onProgress: @escaping (Int) -> Void
|
||
) async throws -> String {
|
||
if let error { throw error }
|
||
onProgress(100)
|
||
uploadedFileNames.append(fileName)
|
||
return "https://oss.example.com/\(fileName)"
|
||
}
|
||
}
|
||
|
||
private func makeAttachment(kind: WildReportAttachmentKind, fileName: String) throws -> WildReportAttachment {
|
||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
|
||
try Data("mock".utf8).write(to: url)
|
||
return WildReportAttachment(
|
||
kind: kind,
|
||
title: fileName,
|
||
localFileURL: url,
|
||
fileName: fileName
|
||
)
|
||
}
|
||
|
||
private func makeRiskMapResponse() -> WildReportRiskMapResponse {
|
||
WildReportRiskMapResponse(
|
||
scenicId: 128,
|
||
scenicName: "伊犁那拉提景区-5A",
|
||
legend: [WildReportRiskLegendItem(markerType: "red", title: "疑似打野")],
|
||
greenMarkerTip: "绿色为景区内摄影师",
|
||
markers: [
|
||
WildReportRiskMapMarkerDTO(
|
||
markerType: "red",
|
||
id: 32,
|
||
latitude: "43.4068",
|
||
longitude: "84.0582",
|
||
locationName: "空中草原观景道",
|
||
title: "线索 ID:32",
|
||
reportTypeText: "疑似打野",
|
||
handleStatus: WildReportDecodedHandleStatus(intValue: 0),
|
||
distanceM: 320,
|
||
createdAt: "2026-07-08 16:06:59"
|
||
),
|
||
WildReportRiskMapMarkerDTO(
|
||
markerType: "yellow",
|
||
id: 33,
|
||
latitude: "43.3652",
|
||
longitude: "84.0385",
|
||
locationName: "塔吾萨尼步道",
|
||
title: "线索 ID:33",
|
||
reportTypeText: "处理中",
|
||
handleStatus: WildReportDecodedHandleStatus(intValue: 1),
|
||
distanceM: 1280,
|
||
createdAt: "2026-07-08 16:08:59"
|
||
),
|
||
WildReportRiskMapMarkerDTO(
|
||
markerType: "green",
|
||
id: 901,
|
||
latitude: "43.4042",
|
||
longitude: "84.0186",
|
||
locationName: "空中草原",
|
||
title: "景区内摄影师",
|
||
distanceM: 430,
|
||
createdAt: "2026-07-08 16:09:59",
|
||
name: "阿依达娜",
|
||
storeName: "空中草原旅拍店",
|
||
online: true,
|
||
activityText: "当前在空中草原区域服务游客拍摄。"
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
private func makeReportListItem(
|
||
id: Int = 32,
|
||
complaintNo: String,
|
||
title: String = "疑似打野摄影师举报",
|
||
reportType: Int = 2,
|
||
reportTypeText: String = "疑似打野",
|
||
handleStatus: Int,
|
||
handleStatusText: String = "待处理"
|
||
) -> WildReportListItem {
|
||
WildReportListItem(
|
||
id: id,
|
||
complaintNo: complaintNo,
|
||
title: title,
|
||
reportType: reportType,
|
||
reportTypeText: reportTypeText,
|
||
desc: "测试说明",
|
||
scenicId: 128,
|
||
scenicName: "伊犁那拉提景区-5A",
|
||
locationName: "伊犁那拉提景区-5A",
|
||
latitude: "32.4299428",
|
||
longitude: "119.4403263",
|
||
handleStatus: handleStatus,
|
||
handleStatusText: handleStatusText,
|
||
createdAt: "2026-07-08 16:06:59"
|
||
)
|
||
}
|
||
|
||
private func makeReportDetailData(
|
||
handleStatus: Int = 0,
|
||
handleStatusText: String = "待处理",
|
||
handleRemark: String = "处理备注",
|
||
handledAt: String = "",
|
||
handler: WildReportDetailHandler? = nil,
|
||
handlerName: String = "",
|
||
handlerPhone: String = "",
|
||
handleEvidences: [WildReportDetailEvidence] = []
|
||
) -> WildReportDetailData {
|
||
WildReportDetailData(
|
||
id: 32,
|
||
complaintNo: "JB20260708917",
|
||
title: "私下收款举报",
|
||
reportType: 1,
|
||
reportTypeText: "私下收款",
|
||
desc: "正式详情说明",
|
||
supplements: [
|
||
WildReportDetailContent(
|
||
type: "supplement",
|
||
id: 7,
|
||
desc: "补充说明",
|
||
time: "2026-07-08 17:06:59",
|
||
timeLabel: "17:06:59",
|
||
evidences: [
|
||
WildReportDetailEvidence(
|
||
fileType: 1,
|
||
fileTypeText: "图片",
|
||
fileURL: "https://example.com/supplement.jpg",
|
||
fileName: "supplement.jpg"
|
||
)
|
||
]
|
||
)
|
||
],
|
||
reportContents: [],
|
||
scenicId: 128,
|
||
scenicName: "伊犁那拉提景区-5A",
|
||
storeUserId: 0,
|
||
contactPhone: "13800138000",
|
||
locationName: "伊犁那拉提景区-5A",
|
||
locationAddress: "江苏省扬州市邗江区瘦西湖路",
|
||
latitude: "32.4299428",
|
||
longitude: "119.4403263",
|
||
handleStatus: handleStatus,
|
||
handleStatusText: handleStatusText,
|
||
handleRemark: handleRemark,
|
||
handledAt: handledAt,
|
||
handler: handler,
|
||
handlerName: handlerName,
|
||
handlerPhone: handlerPhone,
|
||
evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
|
||
evidences: [
|
||
WildReportDetailEvidence(
|
||
fileType: 1,
|
||
fileTypeText: "图片",
|
||
fileURL: "https://example.com/report.jpg",
|
||
fileName: "report.jpg"
|
||
),
|
||
WildReportDetailEvidence(
|
||
fileType: 2,
|
||
fileTypeText: "视频",
|
||
fileURL: "https://example.com/report.mov",
|
||
fileName: "report.mov"
|
||
)
|
||
],
|
||
handleEvidences: handleEvidences,
|
||
paymentScreenshots: [
|
||
WildReportDetailEvidence(
|
||
fileType: 5,
|
||
fileTypeText: "支付截图",
|
||
fileURL: "https://example.com/payment.jpg",
|
||
fileName: "payment.jpg"
|
||
)
|
||
],
|
||
timeline: [
|
||
WildReportDetailTimelineItem(time: "2026-07-08 16:06:59", content: "摄影师提交举报")
|
||
],
|
||
createdAt: "2026-07-08 16:06:59"
|
||
)
|
||
}
|