对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
302 lines
13 KiB
Swift
302 lines
13 KiB
Swift
//
|
|
// HomeViewModelTests.swift
|
|
// suixinkanTests
|
|
//
|
|
|
|
import UIKit
|
|
import XCTest
|
|
@testable import suixinkan
|
|
|
|
@MainActor
|
|
/// 首页 ViewModel 测试。
|
|
final class HomeViewModelTests: XCTestCase {
|
|
|
|
private var appStore: AppStore!
|
|
private var defaults: UserDefaults!
|
|
|
|
override func setUp() {
|
|
defaults = UserDefaults(suiteName: "HomeViewModelTests")!
|
|
defaults.removePersistentDomain(forName: "HomeViewModelTests")
|
|
appStore = AppStore(defaults: defaults)
|
|
appStore.session.userId = "2001"
|
|
appStore.session.accountType = .scenicUser
|
|
appStore.session.roleCode = AppRoleCode.photographer.rawValue
|
|
appStore.session.roleName = "摄影师"
|
|
appStore.session.currentScenicId = 10
|
|
appStore.session.currentScenicName = "测试景区"
|
|
}
|
|
|
|
override func tearDown() {
|
|
appStore.logout()
|
|
super.tearDown()
|
|
}
|
|
|
|
func testMinimalTopRoleFlag() {
|
|
appStore.session.roleCode = AppRoleCode.storeAdmin.rawValue
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
viewModel.refreshLocalDisplayState()
|
|
XCTAssertTrue(viewModel.isMinimalTopRole)
|
|
|
|
appStore.session.roleCode = AppRoleCode.photographer.rawValue
|
|
viewModel.refreshLocalDisplayState()
|
|
XCTAssertFalse(viewModel.isMinimalTopRole)
|
|
}
|
|
|
|
func testLocationReportPromptDefaultsToFixedHomeCopy() {
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
XCTAssertEqual(viewModel.locationInfoText, HomeViewModel.locationReportPromptText)
|
|
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.permissions.savePermissionItems([
|
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
|
HomePermissionItem(id: "2", name: "未知", uri: "unknown_uri"),
|
|
])
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
viewModel.rebuildCommonMenus()
|
|
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["wallet"])
|
|
}
|
|
|
|
func testRebuildCommonMenusPersistsDefaultURIsOnFirstLoad() {
|
|
let menuStore = HomeCommonMenuStore(defaults: defaults)
|
|
appStore.permissions.savePermissionItems([
|
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
|
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
|
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
|
|
HomePermissionItem(id: "4", name: "设置", uri: "system_settings"),
|
|
HomePermissionItem(id: "5", name: "消息", uri: "message_center"),
|
|
])
|
|
let viewModel = HomeViewModel(appStore: appStore, commonMenuStore: menuStore)
|
|
|
|
viewModel.rebuildCommonMenus()
|
|
|
|
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
|
"wallet", "cloud_management", "task_management", "system_settings",
|
|
])
|
|
XCTAssertEqual(
|
|
menuStore.savedCommonURIs(
|
|
accountScope: appStore.session.accountCachePrefix,
|
|
roleCode: appStore.session.roleCode
|
|
),
|
|
["wallet", "cloud_management", "task_management", "system_settings"]
|
|
)
|
|
}
|
|
|
|
func testEmptyRolePermissionShowsPermissionDialog() async throws {
|
|
let responseData = try TestJSON.envelope(data: [RolePermissionResponse]())
|
|
let session = MockURLSession(responses: [responseData])
|
|
let api = HomeAPI(client: APIClient(session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
await viewModel.loadPermissions(api: api, force: true)
|
|
|
|
XCTAssertTrue(viewModel.showPermissionDialog)
|
|
XCTAssertTrue(viewModel.commonMenus.isEmpty)
|
|
}
|
|
|
|
func testPermissionApplyDestinationRoutesReviewingAndRejectedToStatus() async throws {
|
|
let reviewingJSON = """
|
|
{"code":100000,"msg":"success","data":[{"code":"AP001","role_id":1,"role_name":"摄影师","scenic_list":[],"status":1,"status_label":"审核中","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
|
""".data(using: .utf8)!
|
|
let rejectedJSON = """
|
|
{"code":100000,"msg":"success","data":[{"code":"AP002","role_id":1,"role_name":"摄影师","scenic_list":[],"status":3,"status_label":"已驳回","created_at":"","audit_note":"资料不全","audited_at":null,"audited_by":null}]}
|
|
""".data(using: .utf8)!
|
|
let session = MockURLSession(responses: [reviewingJSON, rejectedJSON])
|
|
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
let reviewingDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
|
let rejectedDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
|
XCTAssertEqual(reviewingDestination, .status(applyCode: "AP001"))
|
|
XCTAssertEqual(rejectedDestination, .status(applyCode: "AP002"))
|
|
}
|
|
|
|
func testPermissionApplyDestinationFallsBackToApply() async throws {
|
|
let completedJSON = """
|
|
{"code":100000,"msg":"success","data":[{"code":"AP003","role_id":1,"role_name":"摄影师","scenic_list":[],"status":2,"status_label":"已通过","created_at":"","audit_note":"","audited_at":null,"audited_by":null}]}
|
|
""".data(using: .utf8)!
|
|
let emptyJSON = #"{"code":100000,"msg":"success","data":[]}"#.data(using: .utf8)!
|
|
let session = MockURLSession(responses: [completedJSON, emptyJSON])
|
|
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
let completedDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
|
let emptyDestination = await viewModel.resolvePermissionApplyDestination(api: api)
|
|
XCTAssertEqual(completedDestination, .apply)
|
|
XCTAssertEqual(emptyDestination, .apply)
|
|
}
|
|
|
|
func testPermissionApplyDestinationFallsBackToApplyOnError() async {
|
|
let response = HTTPURLResponse(
|
|
url: URL(string: "https://api-test.zhifly.cn/mock")!,
|
|
statusCode: 500,
|
|
httpVersion: nil,
|
|
headerFields: nil
|
|
)!
|
|
let session = MockURLSession(results: [.success((Data(), response))])
|
|
let api = HomeAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
let destination = await viewModel.resolvePermissionApplyDestination(api: api)
|
|
XCTAssertEqual(destination, .apply)
|
|
}
|
|
|
|
func testRolePermissionBuildsCommonMenusAndStoreCardRole() async throws {
|
|
appStore.session.roleCode = AppRoleCode.storeAdmin.rawValue
|
|
let permissionResponse = RolePermissionResponse(
|
|
role: RoleInfo(
|
|
id: 46,
|
|
name: "店铺管理员",
|
|
roleCode: AppRoleCode.storeAdmin.rawValue,
|
|
notes: nil,
|
|
permission: [
|
|
PermissionItem(id: 1, pid: 0, name: "钱包", uri: "wallet", iconSrc: nil, children: nil),
|
|
PermissionItem(id: 2, pid: 0, name: "云盘", uri: "cloud_management", iconSrc: nil, children: nil),
|
|
]
|
|
),
|
|
scenic: [ScenicInfo(id: 10, name: "测试景区", status: 1)]
|
|
)
|
|
let storeResponse = StoreListResponse(
|
|
total: 1,
|
|
list: [StoreItem(id: 5, scenicId: 10, name: "测试门店", address: "地址", status: 1, statusText: "营业中")]
|
|
)
|
|
|
|
let permissionJSON = try TestJSON.envelope(data: [permissionResponse])
|
|
let storeJSON = try TestJSON.envelope(data: storeResponse)
|
|
let session = MockURLSession(responses: [permissionJSON, storeJSON])
|
|
let api = HomeAPI(client: APIClient(session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
await viewModel.loadPermissions(api: api, force: true)
|
|
|
|
XCTAssertFalse(viewModel.showPermissionDialog)
|
|
XCTAssertTrue(viewModel.isMinimalTopRole)
|
|
XCTAssertEqual(viewModel.commonMenus.count, 2)
|
|
XCTAssertEqual(viewModel.storeItem?.name, "测试门店")
|
|
}
|
|
|
|
func testMarkNeedsPermissionReload() async throws {
|
|
let permissionResponse = RolePermissionResponse(
|
|
role: RoleInfo(
|
|
id: 41,
|
|
name: "摄影师",
|
|
roleCode: AppRoleCode.photographer.rawValue,
|
|
notes: nil,
|
|
permission: [
|
|
PermissionItem(id: 1, pid: 0, name: "钱包", uri: "wallet", iconSrc: nil, children: nil),
|
|
]
|
|
),
|
|
scenic: []
|
|
)
|
|
appStore.permissions.savePermissionItems([HomePermissionItem(id: "1", name: "钱包", uri: "wallet")])
|
|
|
|
let permissionJSON = try TestJSON.envelope(data: [permissionResponse])
|
|
let session = MockURLSession(responses: [permissionJSON])
|
|
let api = HomeAPI(client: APIClient(session: session))
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
viewModel.markNeedsPermissionReload()
|
|
await viewModel.reloadIfNeeded(api: api)
|
|
|
|
XCTAssertEqual(viewModel.commonMenus.map(\.uri), ["wallet"])
|
|
XCTAssertEqual(session.requests.count, 1)
|
|
}
|
|
|
|
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
|
|
appStore.session.currentScenicId = 10
|
|
appStore.session.currentScenicName = ""
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
viewModel.refreshLocalDisplayState()
|
|
|
|
XCTAssertEqual(viewModel.currentScenicName, "")
|
|
XCTAssertEqual(appStore.session.currentScenicId, 0)
|
|
XCTAssertEqual(appStore.session.currentScenicName, "")
|
|
}
|
|
|
|
func testRefreshLocalDisplayStateClearsWhenIdMissing() {
|
|
appStore.session.currentScenicId = 0
|
|
appStore.session.currentScenicName = "残留名称"
|
|
let viewModel = HomeViewModel(appStore: appStore)
|
|
|
|
viewModel.refreshLocalDisplayState()
|
|
|
|
XCTAssertEqual(viewModel.currentScenicName, "")
|
|
XCTAssertEqual(appStore.session.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)
|
|
}
|
|
}
|