feat: update app workflows and permissions
This commit is contained in:
@ -46,6 +46,39 @@ final class CooperationAcquirerViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(message, "请输入验证码")
|
||||
}
|
||||
|
||||
func testCommissionRateDraftFiltersDigitsAndLimitsLength() {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
|
||||
viewModel.updateCommissionRateDraft("a12b345")
|
||||
|
||||
XCTAssertEqual(viewModel.commissionRateDraft, "123")
|
||||
}
|
||||
|
||||
func testCommissionSmsCodeDraftFiltersDigitsAndLimitsLength() {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
|
||||
viewModel.updateCommissionSmsCodeDraft("1a2b345678")
|
||||
|
||||
XCTAssertEqual(viewModel.commissionSmsCodeDraft, "123456")
|
||||
}
|
||||
|
||||
func testRequestCommissionSmsCodeShowsCountdownMessageWhenTimerActive() async {
|
||||
let json = """
|
||||
{"code":100000,"msg":"success","data":{}}
|
||||
""".data(using: .utf8)!
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
viewModel.startEditCommissionRate(CooperativeSalerEntity(saleUserId: 1, salerPhone: "13800138000"))
|
||||
|
||||
await viewModel.requestCommissionSmsCode(api: makeAPI(responses: [json]))
|
||||
await Task.yield()
|
||||
await viewModel.requestCommissionSmsCode(api: makeAPI(responses: []))
|
||||
|
||||
XCTAssertEqual(message, "请在倒计时结束后重新获取")
|
||||
viewModel.dismissCommissionRateDialog()
|
||||
}
|
||||
|
||||
func testProcessScanResultNavigatesWithValidQR() {
|
||||
let viewModel = CooperationAcquirerViewModel(appStore: appStore)
|
||||
var navigatedId: Int?
|
||||
|
||||
@ -63,6 +63,14 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
let item = HomeMenuCatalog.item(for: "report_photographer")
|
||||
|
||||
XCTAssertEqual(item?.title, "举报摄影师")
|
||||
XCTAssertEqual(item?.iconName, "exclamationmark.shield")
|
||||
XCTAssertEqual(item?.iconName, "shield.fill")
|
||||
}
|
||||
|
||||
func testHomeMenuCatalogUsesSyncedSymbols() {
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "space_settings")?.iconName, "person.crop.square.fill")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "wallet")?.iconName, "creditcard.fill")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "sample_management")?.iconName, "point.3.connected.trianglepath.dotted")
|
||||
XCTAssertEqual(HomeMenuCatalog.item(for: "/scenic-queue")?.iconName, "person.3.fill")
|
||||
XCTAssertEqual(HomeMenuItem.moreFunctions.iconName, "ellipsis")
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@ -48,6 +49,33 @@ final class HomeViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.locationInfoText, "您已进入打卡范围")
|
||||
}
|
||||
|
||||
func testQuickActionsOnlineEntrySyncsOfflineStyle() throws {
|
||||
let view = HomeQuickActionsView()
|
||||
|
||||
view.apply(isOnline: false)
|
||||
|
||||
let statusContainer = try statusActionContainer(in: view)
|
||||
XCTAssertEqual(statusContainer.backgroundColor?.homeTestHexRGB, AppColor.cardBackground.homeTestHexRGB)
|
||||
XCTAssertEqual(statusContainer.layer.borderWidth, 1)
|
||||
XCTAssertEqual(statusLabel(in: statusContainer)?.text, "离线")
|
||||
XCTAssertEqual(statusLabel(in: statusContainer)?.textColor.homeTestHexRGB, AppColor.textTabInactive.homeTestHexRGB)
|
||||
XCTAssertEqual(statusIcon(in: statusContainer)?.accessibilityIdentifier, "wifi.slash")
|
||||
XCTAssertEqual(statusIcon(in: statusContainer)?.tintColor.homeTestHexRGB, AppColor.textTabInactive.homeTestHexRGB)
|
||||
}
|
||||
|
||||
func testQuickActionsOnlineEntrySyncsOnlineStyle() throws {
|
||||
let view = HomeQuickActionsView()
|
||||
|
||||
view.apply(isOnline: true)
|
||||
|
||||
let statusContainer = try statusActionContainer(in: view)
|
||||
XCTAssertEqual(statusContainer.backgroundColor?.homeTestHexRGB, AppColor.online.homeTestHexRGB)
|
||||
XCTAssertEqual(statusLabel(in: statusContainer)?.text, "在线")
|
||||
XCTAssertEqual(statusLabel(in: statusContainer)?.textColor.homeTestHexRGB, AppColor.successBackground.homeTestHexRGB)
|
||||
XCTAssertEqual(statusIcon(in: statusContainer)?.accessibilityIdentifier, "wifi")
|
||||
XCTAssertEqual(statusIcon(in: statusContainer)?.tintColor.homeTestHexRGB, AppColor.successBackground.homeTestHexRGB)
|
||||
}
|
||||
|
||||
func testRebuildCommonMenusFiltersUnknownURIs() {
|
||||
appStore.savePermissionItems([
|
||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||
@ -178,4 +206,49 @@ final class HomeViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.currentScenicName, "")
|
||||
XCTAssertEqual(appStore.currentScenicName, "")
|
||||
}
|
||||
|
||||
private func statusActionContainer(in view: HomeQuickActionsView) throws -> UIView {
|
||||
let stackView = try XCTUnwrap(view.subviews.compactMap { $0 as? UIStackView }.first)
|
||||
XCTAssertEqual(stackView.arrangedSubviews.count, 3)
|
||||
return stackView.arrangedSubviews[2]
|
||||
}
|
||||
|
||||
private func statusLabel(in view: UIView) -> UILabel? {
|
||||
if let label = view as? UILabel {
|
||||
return label
|
||||
}
|
||||
for subview in view.subviews {
|
||||
if let label = statusLabel(in: subview) {
|
||||
return label
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func statusIcon(in view: UIView) -> UIImageView? {
|
||||
if let imageView = view as? UIImageView {
|
||||
return imageView
|
||||
}
|
||||
for subview in view.subviews {
|
||||
if let imageView = statusIcon(in: subview) {
|
||||
return imageView
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
|
||||
var homeTestHexRGB: UInt {
|
||||
var red: CGFloat = 0
|
||||
var green: CGFloat = 0
|
||||
var blue: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
let r = Int(round(red * 255))
|
||||
let g = Int(round(green * 255))
|
||||
let b = Int(round(blue * 255))
|
||||
return UInt(r << 16 | g << 8 | b)
|
||||
}
|
||||
}
|
||||
|
||||
42
suixinkanTests/MainTabItemTests.swift
Normal file
42
suixinkanTests/MainTabItemTests.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// MainTabItemTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 主 TabBar 图标与可访问性测试。
|
||||
@MainActor
|
||||
final class MainTabItemTests: XCTestCase {
|
||||
|
||||
func testAppTabsUseRestoredAssetIconsAndBuildImages() {
|
||||
XCTAssertEqual(AppTab.home.unselectedImageName, "tab_home_unselected")
|
||||
XCTAssertEqual(AppTab.home.selectedImageName, "tab_home_selected")
|
||||
XCTAssertEqual(AppTab.orders.unselectedImageName, "tab_order_unselected")
|
||||
XCTAssertEqual(AppTab.statistics.selectedImageName, "tab_data_selected")
|
||||
XCTAssertEqual(AppTab.profile.unselectedImageName, "tab_profile_unselected")
|
||||
|
||||
for tab in AppTab.allCases {
|
||||
let item = tab.makeTabBarItem()
|
||||
XCTAssertEqual(item.title, tab.title)
|
||||
XCTAssertEqual(item.accessibilityIdentifier, "main.tab.\(tab.rawValue)")
|
||||
XCTAssertNotNil(item.image)
|
||||
XCTAssertNotNil(item.selectedImage)
|
||||
XCTAssertEqual(item.image?.renderingMode, .alwaysOriginal)
|
||||
XCTAssertEqual(item.selectedImage?.renderingMode, .alwaysOriginal)
|
||||
}
|
||||
}
|
||||
|
||||
func testScanTabItemBuildsGeneratedImageAndKeepsAccessibility() {
|
||||
let item = MainScanTabItem.makeTabBarItem()
|
||||
|
||||
XCTAssertNil(item.title)
|
||||
XCTAssertEqual(item.accessibilityLabel, "扫码核销")
|
||||
XCTAssertEqual(item.accessibilityIdentifier, "main.scan")
|
||||
XCTAssertNotNil(item.image)
|
||||
XCTAssertNotNil(item.selectedImage)
|
||||
XCTAssertEqual(item.image?.renderingMode, .alwaysOriginal)
|
||||
}
|
||||
}
|
||||
@ -118,6 +118,18 @@ final class MaterialManagementViewModelTests: XCTestCase {
|
||||
XCTAssertNil(api.lastUploadRequest?.id)
|
||||
}
|
||||
|
||||
func testUploadDialogStaysHiddenAfterCompletionProgressCallback() async {
|
||||
let viewModel = MaterialFormViewModel(mode: .create, currentScenicIdProvider: { 3 })
|
||||
let uploader = MockMaterialUploader(urls: ["https://cdn/a.jpg"])
|
||||
|
||||
await viewModel.addLocalMedia([makeUploadItem(fileName: "a.jpg")], uploader: uploader)
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertNil(viewModel.uploadDialogState)
|
||||
XCTAssertFalse(viewModel.uploadedMediaList.first?.isUploading ?? true)
|
||||
XCTAssertEqual(viewModel.uploadedMediaList.first?.uploadProgress, 100)
|
||||
}
|
||||
|
||||
func testEditLoadsDetailAndBuildsEditRequest() async {
|
||||
let api = MockMaterialAPI()
|
||||
api.spotResponse = ScenicSpotListResponse(list: [ScenicSpotItem(id: 9, name: "打卡点")], total: 1)
|
||||
|
||||
@ -47,6 +47,16 @@ final class OrderListUITokensTests: XCTestCase {
|
||||
badge.applyOrderStatus(50, text: "已退款")
|
||||
XCTAssertEqual(badge.backgroundColor?.hexRGB, AppColor.dangerBackground.hexRGB)
|
||||
}
|
||||
|
||||
func testOrderTypeChipIntrinsicSizeWrapsTextWithPadding() {
|
||||
let chip = OrderTypeChipView()
|
||||
chip.apply(text: "订单类型")
|
||||
|
||||
let size = chip.intrinsicContentSize
|
||||
XCTAssertGreaterThan(size.width, "订单类型".size(withAttributes: [.font: UIFont.app(.caption)]).width)
|
||||
XCTAssertGreaterThan(size.height, UIFont.app(.caption).lineHeight)
|
||||
XCTAssertFalse(chip.isHidden)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
|
||||
130
suixinkanTests/OrderRefundDialogValidationTests.swift
Normal file
130
suixinkanTests/OrderRefundDialogValidationTests.swift
Normal file
@ -0,0 +1,130 @@
|
||||
//
|
||||
// OrderRefundDialogValidationTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
final class OrderRefundDialogValidationTests: XCTestCase {
|
||||
|
||||
func testPhotographerOrderRequiresRefundReason() {
|
||||
let result = OrderRefundDialogValidation.validate(
|
||||
kind: .photographerOrder,
|
||||
mode: .full,
|
||||
amountText: "",
|
||||
reason: "",
|
||||
fullRefundAmount: "88.00",
|
||||
maxRefundAmount: "88.00"
|
||||
)
|
||||
|
||||
XCTAssertEqual(failureMessage(from: result), "请输入退款原因")
|
||||
}
|
||||
|
||||
func testPhotographerPartialRefundRequiresAmount() {
|
||||
let result = OrderRefundDialogValidation.validate(
|
||||
kind: .photographerOrder,
|
||||
mode: .partial,
|
||||
amountText: "",
|
||||
reason: "用户申请退款",
|
||||
fullRefundAmount: "88.00",
|
||||
maxRefundAmount: "88.00"
|
||||
)
|
||||
|
||||
XCTAssertEqual(failureMessage(from: result), "请输入退款金额")
|
||||
}
|
||||
|
||||
func testStoreOrderRefundReasonLengthValidation() {
|
||||
let shortResult = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .full,
|
||||
amountText: "",
|
||||
reason: "太短",
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
XCTAssertEqual(failureMessage(from: shortResult), "退款原因至少5个字符")
|
||||
|
||||
let longResult = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .full,
|
||||
amountText: "",
|
||||
reason: String(repeating: "a", count: 256),
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
XCTAssertEqual(failureMessage(from: longResult), "退款原因最多255个字符")
|
||||
}
|
||||
|
||||
func testStoreOrderPartialRefundAmountValidation() {
|
||||
let emptyResult = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .partial,
|
||||
amountText: "",
|
||||
reason: "用户申请退款",
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
XCTAssertEqual(failureMessage(from: emptyResult), "请输入退款金额")
|
||||
|
||||
let invalidResult = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .partial,
|
||||
amountText: "0",
|
||||
reason: "用户申请退款",
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
XCTAssertEqual(failureMessage(from: invalidResult), "请输入有效的退款金额")
|
||||
|
||||
let overflowResult = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .partial,
|
||||
amountText: "100.00",
|
||||
reason: "用户申请退款",
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
XCTAssertEqual(failureMessage(from: overflowResult), "退款金额不能大于实际支付金额")
|
||||
}
|
||||
|
||||
func testStoreOrderFullRefundUsesZeroAmount() throws {
|
||||
let result = OrderRefundDialogValidation.validate(
|
||||
kind: .storeOrder,
|
||||
mode: .full,
|
||||
amountText: "99.00",
|
||||
reason: "用户申请退款",
|
||||
fullRefundAmount: "0",
|
||||
maxRefundAmount: "99.00"
|
||||
)
|
||||
|
||||
let payload = try successPayload(from: result)
|
||||
XCTAssertEqual(payload.refundType, OrderRefundMode.full.rawValue)
|
||||
XCTAssertEqual(payload.refundAmount, "0")
|
||||
XCTAssertEqual(payload.refundReason, "用户申请退款")
|
||||
}
|
||||
|
||||
func testMoneyInputKeepsSingleDotAndTwoDecimalPlaces() {
|
||||
XCTAssertEqual(OrderRefundDialogValidation.filterMoneyInput("12..345a6", maxLength: 10), "12.34")
|
||||
XCTAssertEqual(OrderRefundDialogValidation.filterMoneyInput("12345678901", maxLength: 10), "1234567890")
|
||||
}
|
||||
|
||||
private func failureMessage(from result: Result<OrderRefundDialogPayload, OrderRefundDialogValidationError>) -> String? {
|
||||
if case .failure(let error) = result {
|
||||
return error.message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func successPayload(
|
||||
from result: Result<OrderRefundDialogPayload, OrderRefundDialogValidationError>
|
||||
) throws -> OrderRefundDialogPayload {
|
||||
switch result {
|
||||
case .success(let payload):
|
||||
return payload
|
||||
case .failure(let error):
|
||||
XCTFail("Expected success, got failure: \(error.message)")
|
||||
throw NSError(domain: "OrderRefundDialogValidationTests", code: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WechatOpenSDK
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@ -24,6 +25,24 @@ final class WeChatShareServiceTests: XCTestCase {
|
||||
XCTAssertEqual(WeChatShareScene.favorite.wxSceneValue, 2)
|
||||
}
|
||||
|
||||
/// 测试小程序版本映射到微信 SDK 值。
|
||||
func testMiniProgramTypeMapsToWXValues() {
|
||||
XCTAssertEqual(WeChatMiniProgramType.release.rawValue, 0)
|
||||
XCTAssertEqual(WeChatMiniProgramType.test.rawValue, 1)
|
||||
XCTAssertEqual(WeChatMiniProgramType.preview.rawValue, 2)
|
||||
}
|
||||
|
||||
/// 测试 Debug 包使用体验版小程序,Release 包保持传入版本。
|
||||
func testMiniProgramTypeResolvesForCurrentBuild() {
|
||||
#if DEBUG
|
||||
XCTAssertEqual(WeChatMiniProgramType.release.resolvedForCurrentBuild, .preview)
|
||||
XCTAssertEqual(WeChatMiniProgramType.test.resolvedForCurrentBuild, .preview)
|
||||
#else
|
||||
XCTAssertEqual(WeChatMiniProgramType.release.resolvedForCurrentBuild, .release)
|
||||
XCTAssertEqual(WeChatMiniProgramType.test.resolvedForCurrentBuild, .test)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 测试微信错误码解析。
|
||||
func testShareResultFromWXErrorCode() {
|
||||
XCTAssertEqual(WeChatShareResult.fromWXErrorCode(0), .success)
|
||||
@ -45,6 +64,23 @@ final class WeChatShareServiceTests: XCTestCase {
|
||||
XCTAssertLessThanOrEqual(thumbData.count, WeChatThumbImageProcessor.maxThumbBytes)
|
||||
}
|
||||
|
||||
/// 测试小程序高清预览图压缩到微信限制内。
|
||||
func testMiniProgramHDImageProcessorKeepsDataUnderLimit() throws {
|
||||
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 1200, height: 800))
|
||||
let image = renderer.image { context in
|
||||
UIColor.systemGreen.setFill()
|
||||
context.fill(CGRect(x: 0, y: 0, width: 1200, height: 800))
|
||||
UIColor.white.setFill()
|
||||
context.fill(CGRect(x: 160, y: 120, width: 880, height: 560))
|
||||
UIColor.systemBlue.setFill()
|
||||
context.fill(CGRect(x: 260, y: 220, width: 680, height: 360))
|
||||
}
|
||||
|
||||
let imageData = try XCTUnwrap(WeChatThumbImageProcessor.makeMiniProgramHDImageData(from: image))
|
||||
XCTAssertLessThanOrEqual(imageData.count, WeChatThumbImageProcessor.maxMiniProgramHDImageBytes)
|
||||
XCTAssertNil(WeChatThumbImageProcessor.makeMiniProgramHDImageData(from: image, maxBytes: 0))
|
||||
}
|
||||
|
||||
/// 测试空标题和空链接会直接返回参数错误。
|
||||
func testShareWebPageRejectsInvalidPayloadBeforeSending() {
|
||||
var results: [WeChatShareResult] = []
|
||||
@ -66,4 +102,90 @@ final class WeChatShareServiceTests: XCTestCase {
|
||||
.invalidPayload("分享链接不能为空"),
|
||||
])
|
||||
}
|
||||
|
||||
/// 测试小程序分享请求字段填充。
|
||||
func testMakeMiniProgramRequestFillsWXObject() throws {
|
||||
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 600, height: 400))
|
||||
let image = renderer.image { context in
|
||||
UIColor.systemOrange.setFill()
|
||||
context.fill(CGRect(x: 0, y: 0, width: 600, height: 400))
|
||||
}
|
||||
let payload = WeChatMiniProgramSharePayload(
|
||||
title: " 举报详情 ",
|
||||
description: "查看处理进度",
|
||||
webpageURL: " https://www.zhifly.cn/report/40 ",
|
||||
userName: " gh_xxxxxxxx ",
|
||||
path: " /pages/report/detail?id=40 ",
|
||||
miniProgramType: .preview,
|
||||
withShareTicket: true,
|
||||
disableForward: true
|
||||
)
|
||||
|
||||
let request = try XCTUnwrap(try WeChatShareService.makeMiniProgramRequest(payload, scene: .session, previewImage: image).get())
|
||||
let message = try XCTUnwrap(request.message)
|
||||
let miniProgramObject = try XCTUnwrap(message.mediaObject as? WXMiniProgramObject)
|
||||
|
||||
XCTAssertFalse(request.bText)
|
||||
XCTAssertEqual(request.scene, WeChatShareScene.session.wxSceneValue)
|
||||
XCTAssertEqual(message.title, "举报详情")
|
||||
XCTAssertEqual(message.description, "查看处理进度")
|
||||
XCTAssertEqual(miniProgramObject.webpageUrl, "https://www.zhifly.cn/report/40")
|
||||
XCTAssertEqual(miniProgramObject.userName, "gh_xxxxxxxx")
|
||||
XCTAssertEqual(miniProgramObject.path, "/pages/report/detail?id=40")
|
||||
XCTAssertEqual(miniProgramObject.miniProgramType.rawValue, UInt(WeChatMiniProgramType.preview.resolvedForCurrentBuild.rawValue))
|
||||
XCTAssertEqual(miniProgramObject.withShareTicket, true)
|
||||
XCTAssertEqual(miniProgramObject.disableForward, true)
|
||||
XCTAssertLessThanOrEqual(miniProgramObject.hdImageData?.count ?? 0, WeChatThumbImageProcessor.maxMiniProgramHDImageBytes)
|
||||
}
|
||||
|
||||
/// 测试小程序分享参数校验。
|
||||
func testMakeMiniProgramRequestRejectsInvalidPayload() {
|
||||
let valid = WeChatMiniProgramSharePayload(
|
||||
title: "举报详情",
|
||||
description: "查看处理进度",
|
||||
webpageURL: "https://www.zhifly.cn/report/40",
|
||||
userName: "gh_xxxxxxxx"
|
||||
)
|
||||
let longURL = "https://www.zhifly.cn/" + String(repeating: "a", count: 1025)
|
||||
|
||||
XCTAssertEqual(
|
||||
WeChatShareService.makeMiniProgramRequest(valid, scene: .timeline).failure,
|
||||
.invalidPayload("小程序仅支持分享到微信好友")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
WeChatShareService.makeMiniProgramRequest(
|
||||
WeChatMiniProgramSharePayload(title: " ", description: "", webpageURL: valid.webpageURL, userName: valid.userName),
|
||||
scene: .session
|
||||
).failure,
|
||||
.invalidPayload("分享标题不能为空")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
WeChatShareService.makeMiniProgramRequest(
|
||||
WeChatMiniProgramSharePayload(title: valid.title, description: "", webpageURL: " ", userName: valid.userName),
|
||||
scene: .session
|
||||
).failure,
|
||||
.invalidPayload("小程序兼容网页链接不能为空")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
WeChatShareService.makeMiniProgramRequest(
|
||||
WeChatMiniProgramSharePayload(title: valid.title, description: "", webpageURL: longURL, userName: valid.userName),
|
||||
scene: .session
|
||||
).failure,
|
||||
.invalidPayload("小程序兼容网页链接不能超过1024字节")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
WeChatShareService.makeMiniProgramRequest(
|
||||
WeChatMiniProgramSharePayload(title: valid.title, description: "", webpageURL: valid.webpageURL, userName: " "),
|
||||
scene: .session
|
||||
).failure,
|
||||
.invalidPayload("小程序原始 ID 不能为空")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Result where Success == SendMessageToWXReq, Failure == WeChatShareResult {
|
||||
var failure: WeChatShareResult? {
|
||||
guard case .failure(let result) = self else { return nil }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,6 +233,17 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
"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": [
|
||||
{
|
||||
@ -248,6 +259,24 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
"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,
|
||||
@ -276,6 +305,55 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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 {
|
||||
@ -472,6 +550,106 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
XCTAssertEqual(detail.supplementRecords.first?.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)
|
||||
@ -731,6 +909,61 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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()
|
||||
@ -1023,7 +1256,28 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
let record = makeReportListItem(complaintNo: "JB20260708917", handleStatus: 0).toRecord()
|
||||
let viewModel = WildPhotographerReportDetailViewModel(record: record)
|
||||
let api = MockWildPhotographerReportAPI()
|
||||
api.reportDetailResponses = [makeReportDetailData()]
|
||||
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)
|
||||
|
||||
@ -1032,6 +1286,10 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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 {
|
||||
@ -1048,6 +1306,27 @@ final class WildPhotographerReportTests: XCTestCase {
|
||||
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",
|
||||
@ -1087,6 +1366,9 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
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] = []
|
||||
@ -1150,6 +1432,12 @@ private final class MockWildPhotographerReportAPI: WildPhotographerReportServing
|
||||
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 {
|
||||
@ -1312,7 +1600,13 @@ private func makeReportListItem(
|
||||
|
||||
private func makeReportDetailData(
|
||||
handleStatus: Int = 0,
|
||||
handleStatusText: String = "待处理"
|
||||
handleStatusText: String = "待处理",
|
||||
handleRemark: String = "处理备注",
|
||||
handledAt: String = "",
|
||||
handler: WildReportDetailHandler? = nil,
|
||||
handlerName: String = "",
|
||||
handlerPhone: String = "",
|
||||
handleEvidences: [WildReportDetailEvidence] = []
|
||||
) -> WildReportDetailData {
|
||||
WildReportDetailData(
|
||||
id: 32,
|
||||
@ -1349,7 +1643,11 @@ private func makeReportDetailData(
|
||||
longitude: "119.4403263",
|
||||
handleStatus: handleStatus,
|
||||
handleStatusText: handleStatusText,
|
||||
handleRemark: "处理备注",
|
||||
handleRemark: handleRemark,
|
||||
handledAt: handledAt,
|
||||
handler: handler,
|
||||
handlerName: handlerName,
|
||||
handlerPhone: handlerPhone,
|
||||
evidenceSummary: WildReportEvidenceSummary(imageCount: 1, videoCount: 1),
|
||||
evidences: [
|
||||
WildReportDetailEvidence(
|
||||
@ -1365,6 +1663,7 @@ private func makeReportDetailData(
|
||||
fileName: "report.mov"
|
||||
)
|
||||
],
|
||||
handleEvidences: handleEvidences,
|
||||
paymentScreenshots: [
|
||||
WildReportDetailEvidence(
|
||||
fileType: 5,
|
||||
|
||||
Reference in New Issue
Block a user