Merge branch 'dev_1_2_1' into xh_test

This commit is contained in:
2026-08-03 10:48:41 +08:00
47 changed files with 5386 additions and 2314 deletions
+36
View File
@@ -62,6 +62,7 @@ final class AuthModelsTests: XCTestCase {
XCTAssertEqual(response.token, "person-temp-token")
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"])
}
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
@@ -137,4 +138,39 @@ final class AuthModelsTests: XCTestCase {
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
}
func testV9AccountModelsIgnoreBackendIdentifiersAndUseRealNameFallback() throws {
let json = """
{
"token": "person-temp-token",
"scenic_users": [
{
"account_type": "scenic_user",
"ss_user_id": 101,
"username": "backend_scenic_account",
"user_name": "backend_scenic_unique_id",
"real_name": "景区真实姓名"
}
],
"store_users": [
{
"account_type": "store_user",
"store_user_id": 201,
"username": "backend_store_account",
"user_name": "backend_store_unique_id",
"real_name": "门店真实姓名"
}
]
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(V9AuthResponse.self, from: json)
XCTAssertEqual(response.scenicUsers.first?.realName, "景区真实姓名")
XCTAssertEqual(response.scenicUsers.first?.displayName, "景区真实姓名")
XCTAssertEqual(response.scenicUsers.first?.toAccountSwitchAccount().title, "景区真实姓名")
XCTAssertEqual(response.storeUsers.first?.realName, "门店真实姓名")
XCTAssertEqual(response.storeUsers.first?.displayName, "门店真实姓名")
XCTAssertEqual(response.storeUsers.first?.toAccountSwitchAccount().title, "门店真实姓名")
}
}
+277
View File
@@ -0,0 +1,277 @@
//
// CommissionRateLogTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
/// 获客员分成比例修改记录模型、接口与分页行为测试。
@MainActor
final class CommissionRateLogTests: XCTestCase {
func testModelDecodesCompleteRecordAndBuildsBusinessDisplayText() throws {
let data = """
{
"list": [{
"id": 1,
"binding_id": 10,
"sale_user_id": 456,
"store_user_id": 732,
"before_rate": 10,
"before_rate_label": "10%(旧)",
"after_rate": 20,
"after_rate_label": "20%(新)",
"operator_type": "store_user",
"operator_id": 732,
"operator_name": "李师傅",
"remark": " 调整合作比例 ",
"created_at": "2026-07-23 11:00:00"
}],
"total": 1,
"page": 1,
"page_size": 10
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data)
let item = try XCTUnwrap(response.list.first)
XCTAssertEqual(response.pageSize, 10)
XCTAssertEqual(item.bindingId, 10)
XCTAssertEqual(item.saleUserId, 456)
XCTAssertEqual(item.storeUserId, 732)
XCTAssertEqual(item.operatorType, "store_user")
XCTAssertEqual(item.operatorId, 732)
XCTAssertEqual(item.displayBeforeRate, "10%(旧)")
XCTAssertEqual(item.displayAfterRate, "20%(新)")
XCTAssertEqual(item.metadataLine, "2026-07-23 11:00")
XCTAssertEqual(item.displayRemark, "调整合作比例")
}
func testDisplayFallsBackToNumericRateUnknownOperatorAndRawTime() throws {
let data = """
{
"list": [{
"id": 2,
"binding_id": 0,
"sale_user_id": 456,
"store_user_id": 0,
"before_rate": 5,
"before_rate_label": "",
"after_rate": 15,
"after_rate_label": " ",
"operator_type": "",
"operator_id": 0,
"operator_name": "",
"remark": " ",
"created_at": "服务端时间"
}],
"total": 1,
"page": 1,
"page_size": 10
}
""".data(using: .utf8)!
let item = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data).list[0]
XCTAssertEqual(item.displayBeforeRate, "5%")
XCTAssertEqual(item.displayAfterRate, "15%")
XCTAssertEqual(item.metadataLine, "服务端时间")
XCTAssertNil(item.displayRemark)
}
func testAPIBuildsPathAndClampsPaginationParameters() async throws {
let session = MockURLSession(responses: [envelope(list: [], total: 0, page: 1)])
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
_ = try await api.saleUserCommissionRateLogs(saleUserId: 456, page: 0, pageSize: 99)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/commission-rate/logs")
let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
XCTAssertEqual(query?.first { $0.name == "sale_user_id" }?.value, "456")
XCTAssertEqual(query?.first { $0.name == "page" }?.value, "1")
XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "50")
}
func testViewModelRetriesFailedPageAndDeduplicatesItems() async throws {
let page1 = envelope(
list: [
record(id: 1, before: 10, after: 20),
record(id: 1, before: 10, after: 20),
record(id: 2, before: 20, after: 30),
],
total: 3,
page: 1
)
let page2 = envelope(
list: [
record(id: 2, before: 20, after: 30),
record(id: 3, before: 30, after: 40),
],
total: 3,
page: 2
)
let response = HTTPURLResponse(
url: URL(string: "https://api-test.zhifly.cn/mock")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
let session = MockURLSession(results: [
.success((page1, response)),
.failure(URLError(.timedOut)),
.success((page2, response)),
])
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
let viewModel = CommissionRateLogViewModel(saleUserId: 456)
await viewModel.refresh(api: api)
await viewModel.loadMore(api: api)
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
XCTAssertTrue(viewModel.canLoadMore)
XCTAssertFalse(viewModel.isLoadingMore)
await viewModel.loadMore(api: api)
XCTAssertEqual(viewModel.items.map(\.id), [1, 2, 3])
XCTAssertFalse(viewModel.canLoadMore)
let requestedPages = session.requests.compactMap { request in
URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?
.queryItems?
.first { $0.name == "page" }?
.value
}
XCTAssertEqual(requestedPages, ["1", "2", "2"])
}
func testTimelineCellOnlyMarksLatestRecord() {
let item = CommissionRateLogEntity(
id: 1,
bindingId: 10,
saleUserId: 456,
storeUserId: 732,
beforeRate: 10,
beforeRateLabel: "10%",
afterRate: 20,
afterRateLabel: "20%",
operatorType: "store_user",
operatorId: 732,
operatorName: "李师傅",
remark: "",
createdAt: "2026-07-23 11:00:00"
)
let cell = CommissionRateLogTimelineCell(style: .default, reuseIdentifier: nil)
cell.configure(with: item, isLatest: true)
XCTAssertTrue(cell.accessibilityLabel?.contains("最新记录") == true)
XCTAssertFalse(cell.accessibilityLabel?.contains("李师傅") == true)
cell.configure(with: item, isLatest: false)
XCTAssertFalse(cell.accessibilityLabel?.contains("最新记录") == true)
}
func testPageTitleUsesAcquirerNameAndFallsBackWhenEmpty() {
let namedController = CommissionRateLogViewController(
saleUserId: 456,
acquirerName: " 获客员小王 "
)
namedController.setupNavigationBar()
XCTAssertEqual(namedController.title, "给获客员小王的分成记录")
let fallbackController = CommissionRateLogViewController(
saleUserId: 456,
acquirerName: " "
)
fallbackController.setupNavigationBar()
XCTAssertEqual(fallbackController.title, "修改记录")
}
func testAcquirerCardUsesUnifiedActionsAndBusinessHierarchy() throws {
let cell = CooperationAcquirerCell(style: .default, reuseIdentifier: nil)
cell.configure(
with: CooperativeSalerEntity(
saleUserId: 456,
name: "获客员小王",
salerPhone: "13800138000",
commissionRate: 20,
commissionRateLabel: "20%",
bindTime: "2026-07-23 11:00:00"
)
)
let fittingSize = cell.contentView.systemLayoutSizeFitting(
CGSize(width: 390, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
cell.frame = CGRect(x: 0, y: 0, width: 390, height: fittingSize.height)
cell.layoutIfNeeded()
let remarkButton = try XCTUnwrap(
subview(in: cell, identifier: "cooperation_acquirer_edit_remark") as? UIButton
)
let logButton = try XCTUnwrap(
subview(in: cell, identifier: "cooperation_acquirer_commission_logs") as? UIButton
)
let commissionButton = try XCTUnwrap(
subview(in: cell, identifier: "cooperation_acquirer_edit_commission") as? UIButton
)
let bindTimeLabel = try XCTUnwrap(
subview(in: cell, identifier: "cooperation_acquirer_bind_time") as? UILabel
)
XCTAssertEqual(remarkButton.bounds.height, AppSpacing.minTouchTarget, accuracy: 0.5)
XCTAssertEqual(logButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5)
XCTAssertEqual(commissionButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5)
XCTAssertEqual(logButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5)
XCTAssertEqual(commissionButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5)
XCTAssertEqual(bindTimeLabel.text, "绑定于 2026-07-23 11:00")
}
private func envelope(list: [[String: Any]], total: Int, page: Int) -> Data {
let object: [String: Any] = [
"code": 100000,
"msg": "success",
"data": [
"list": list,
"total": total,
"page": page,
"page_size": 10,
],
]
return try! JSONSerialization.data(withJSONObject: object)
}
private func record(id: Int, before: Int, after: Int) -> [String: Any] {
[
"id": id,
"binding_id": 10,
"sale_user_id": 456,
"store_user_id": 732,
"before_rate": before,
"before_rate_label": "\(before)%",
"after_rate": after,
"after_rate_label": "\(after)%",
"operator_type": "store_user",
"operator_id": 732,
"operator_name": "李师傅",
"remark": "",
"created_at": "2026-07-23 11:00:00",
]
}
private func subview(in root: UIView, identifier: String) -> UIView? {
if root.accessibilityIdentifier == identifier {
return root
}
for child in root.subviews {
if let match = subview(in: child, identifier: identifier) {
return match
}
}
return nil
}
}
@@ -0,0 +1,204 @@
//
// CoreLocationProviderTests.swift
// suixinkanTests
//
import CoreLocation
import XCTest
@testable import suixinkan
/// Core Location fallback 的授权、定位与逆地理编码测试。
@MainActor
final class CoreLocationProviderTests: XCTestCase {
func testPrivacyAgreementIsRequiredBeforeLocation() async {
let context = makeContext(privacyAccepted: false)
await assertLocationError(.privacyNotAccepted) {
_ = try await context.provider.requestCoordinate()
}
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
}
func testDeniedAuthorizationReturnsPermissionError() async {
let context = makeContext(authorizationStatus: .denied)
await assertLocationError(.permissionDenied) {
_ = try await context.provider.requestCoordinate()
}
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
}
func testNotDeterminedAuthorizationContinuesAfterGrant() async throws {
let coordinate = CLLocationCoordinate2D(latitude: 30.2741, longitude: 120.1551)
let context = makeContext(
authorizationStatus: .notDetermined,
requestedAuthorizationStatus: .authorizedWhenInUse,
locationResult: .success(CLLocation(
latitude: coordinate.latitude,
longitude: coordinate.longitude
))
)
let result = try await context.provider.requestCoordinate(
desiredAccuracy: kCLLocationAccuracyHundredMeters
)
XCTAssertEqual(context.manager.requestAuthorizationCallCount, 1)
XCTAssertEqual(context.manager.requestLocationCallCount, 1)
XCTAssertEqual(context.manager.desiredAccuracy, kCLLocationAccuracyHundredMeters)
XCTAssertEqual(result.latitude, coordinate.latitude, accuracy: 0.000_001)
XCTAssertEqual(result.longitude, coordinate.longitude, accuracy: 0.000_001)
}
func testLocationFailureUsesExistingBusinessError() async {
let context = makeContext(locationResult: .failure)
await assertLocationError(.locationFailed) {
_ = try await context.provider.requestCoordinate()
}
}
func testSnapshotContainsReverseGeocodedAddress() async throws {
let context = makeContext(
locationResult: .success(CLLocation(latitude: 31.2304, longitude: 121.4737)),
geocodedAddress: "上海市黄浦区"
)
let snapshot = try await context.provider.requestSnapshot()
XCTAssertEqual(snapshot.latitude, 31.2304, accuracy: 0.000_001)
XCTAssertEqual(snapshot.longitude, 121.4737, accuracy: 0.000_001)
XCTAssertEqual(snapshot.address, "上海市黄浦区")
XCTAssertEqual(context.geocoder.requestedCoordinates.count, 1)
}
func testSnapshotFallsBackToEmptyAddressWhenGeocoderHasNoResult() async throws {
let context = makeContext(
locationResult: .success(CLLocation(latitude: 39.9042, longitude: 116.4074)),
geocodedAddress: ""
)
let snapshot = try await context.provider.requestSnapshot()
XCTAssertEqual(snapshot.address, "")
}
private func makeContext(
privacyAccepted: Bool = true,
authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
requestedAuthorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
locationResult: FakeCoreLocationManager.LocationResult = .success(
CLLocation(latitude: 39.9042, longitude: 116.4074)
),
geocodedAddress: String = "北京市东城区"
) -> TestContext {
let suiteName = "CoreLocationProviderTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
let appStore = AppStore(defaults: defaults)
appStore.session.privacyAgreementAccepted = privacyAccepted
let manager = FakeCoreLocationManager(
authorizationStatus: authorizationStatus,
requestedAuthorizationStatus: requestedAuthorizationStatus,
locationResult: locationResult
)
let geocoder = FakeLocationAddressGeocoder(address: geocodedAddress)
let provider = CoreLocationProvider(
appStore: appStore,
manager: manager,
geocoder: geocoder
)
return TestContext(provider: provider, manager: manager, geocoder: geocoder)
}
private func assertLocationError(
_ expected: LocationProviderError,
operation: () async throws -> Void
) async {
do {
try await operation()
XCTFail("Expected \(expected)")
} catch {
XCTAssertEqual(error as? LocationProviderError, expected)
}
}
}
/// Core Location fallback 测试依赖集合。
@MainActor
private struct TestContext {
let provider: CoreLocationProvider
let manager: FakeCoreLocationManager
let geocoder: FakeLocationAddressGeocoder
}
/// 可控的 `CLLocationManager` 替身。
@MainActor
private final class FakeCoreLocationManager: CoreLocationManaging {
/// 单次定位请求的模拟结果。
enum LocationResult {
case success(CLLocation)
case failure
}
var authorizationStatus: CLAuthorizationStatus
var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
weak var delegate: CLLocationManagerDelegate?
private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0
private let requestedAuthorizationStatus: CLAuthorizationStatus
private let locationResult: LocationResult
init(
authorizationStatus: CLAuthorizationStatus,
requestedAuthorizationStatus: CLAuthorizationStatus,
locationResult: LocationResult
) {
self.authorizationStatus = authorizationStatus
self.requestedAuthorizationStatus = requestedAuthorizationStatus
self.locationResult = locationResult
}
func requestWhenInUseAuthorization() {
requestAuthorizationCallCount += 1
authorizationStatus = requestedAuthorizationStatus
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
}
func requestLocation() {
requestLocationCallCount += 1
let callbackManager = CLLocationManager()
switch locationResult {
case let .success(location):
delegate?.locationManager?(callbackManager, didUpdateLocations: [location])
case .failure:
delegate?.locationManager?(
callbackManager,
didFailWithError: CLError(.locationUnknown)
)
}
}
}
/// 可控的逆地理编码替身。
@MainActor
private final class FakeLocationAddressGeocoder: LocationAddressGeocoding {
private(set) var requestedCoordinates: [CLLocationCoordinate2D] = []
private let address: String
init(address: String) {
self.address = address
}
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
requestedCoordinates.append(CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude
))
return address
}
}
+22 -6
View File
@@ -18,20 +18,20 @@ final class HomeMenuIconFactoryTests: XCTestCase {
XCTAssertNil(UIImage(systemName: "home_menu_space"))
}
func testSystemSymbolsUseFixedCanvas() {
func testSystemSymbolsPreserveSymbolRepresentation() {
let iconNames = [
"ellipsis",
"dot.radiowaves.left.and.right",
"rectangle.stack.badge.plus",
"mappin.circle.fill",
]
for iconName in iconNames {
let image = HomeMenuIconFactory.image(named: iconName)
XCTAssertNotNil(image)
XCTAssertEqual(image?.size, HomeMenuIconFactory.canvasSize)
XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
XCTAssertFalse(image?.isSymbolImage ?? true)
XCTAssertTrue(image?.isSymbolImage ?? false)
XCTAssertNotNil(image?.symbolConfiguration)
}
}
@@ -39,6 +39,13 @@ final class HomeMenuIconFactoryTests: XCTestCase {
XCTAssertNotNil(HomeMenuIconFactory.image(named: "dot.radiowaves.left.and.right"))
XCTAssertNotNil(HomeMenuIconFactory.image(named: "rectangle.stack.badge.plus"))
}
func testLocationReportIconKeepsSystemSymbolLayers() {
let image = HomeMenuIconFactory.image(named: "mappin.circle.fill")
XCTAssertNotNil(image)
XCTAssertTrue(image?.isSymbolImage ?? false)
}
}
/// 首页常用应用 cell 的图标布局与复用测试。
@@ -50,6 +57,7 @@ final class HomeMenuCellTests: XCTestCase {
let menus = [
HomeMenuItem(uri: "space", title: "空间设置", iconName: "home_menu_space"),
HomeMenuItem(uri: "live", title: "直播管理", iconName: "dot.radiowaves.left.and.right"),
HomeMenuItem(uri: "location", title: "位置上报", iconName: "mappin.circle.fill"),
HomeMenuItem(uri: "more", title: "更多功能", iconName: "ellipsis"),
]
@@ -60,11 +68,19 @@ final class HomeMenuCellTests: XCTestCase {
cell.layoutIfNeeded()
let iconView = try XCTUnwrap(findImageView(in: cell, identifier: menu.iconName))
XCTAssertEqual(iconView.bounds.size, CGSize(width: 24, height: 24))
let alignmentSize = iconView.alignmentRect(forFrame: iconView.frame).size
XCTAssertEqual(alignmentSize.width, 24, accuracy: 0.001)
XCTAssertEqual(alignmentSize.height, 24, accuracy: 0.001)
XCTAssertEqual(iconView.superview?.bounds.size, CGSize(width: 40, height: 40))
XCTAssertEqual(iconView.image?.size, HomeMenuIconFactory.canvasSize)
XCTAssertEqual(iconView.contentMode, .center)
XCTAssertNil(iconView.preferredSymbolConfiguration)
if menu.iconName == "home_menu_space" {
XCTAssertEqual(iconView.image?.size, HomeMenuIconFactory.canvasSize)
XCTAssertFalse(iconView.image?.isSymbolImage ?? true)
} else {
XCTAssertTrue(iconView.image?.isSymbolImage ?? false)
}
}
}
@@ -69,6 +69,26 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.showAmountDialog)
}
func testPayCodeLoadingStateCoversInitialRenderAndRequest() async {
let payCodeJSON = """
{"code":100000,"msg":"success","data":{"static_pay_url":"https://pay.example.com/static","dynamic_pay_url":"https://pay.example.com/dynamic"}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [payCodeJSON])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
var loadingStates = [viewModel.isPayCodeLoading]
viewModel.onStateChange = {
loadingStates.append(viewModel.isPayCodeLoading)
}
await viewModel.loadPayCode(api: api)
XCTAssertEqual(loadingStates.first, true)
XCTAssertTrue(loadingStates.dropLast().allSatisfy { $0 })
XCTAssertEqual(loadingStates.last, false)
XCTAssertTrue(viewModel.hasPayCode)
}
func testToggleReceiveVoicePersistsInAppStore() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
viewModel.refreshLocalState()
@@ -79,8 +99,9 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
}
func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() {
appStore.session.userName = "摄影师小王"
func testNalatiAccountInfoUsesPhotographerRealNameAndAllPermissionStores() {
appStore.session.userName = "摄影师昵称"
appStore.session.realName = "摄影师真实姓名"
appStore.permissions.saveRolePermissionList([
RolePermissionResponse(
role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"),
@@ -93,10 +114,19 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
XCTAssertEqual(viewModel.displayPhotographerName, "摄影师小王")
XCTAssertEqual(viewModel.displayPhotographerName, "摄影师真实姓名")
XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店")
}
func testPhotographerNameDoesNotFallBackToNicknameWhenRealNameMissing() {
appStore.session.userName = "摄影师昵称"
appStore.session.realName = " "
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
XCTAssertEqual(viewModel.displayPhotographerName, "-")
}
func testLoadPayCodeClearsQRWhenScenicMissing() async {
appStore.session.currentScenicId = 0
let session = MockURLSession(responses: [])
@@ -105,6 +135,7 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
await viewModel.loadPayCode(api: api)
XCTAssertFalse(viewModel.isPayCodeLoading)
XCTAssertFalse(viewModel.hasPayCode)
XCTAssertEqual(session.requests.count, 0)
}
+4 -3
View File
@@ -55,11 +55,12 @@ final class PaymentModelsTests: XCTestCase {
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
}
func testPaymentAccountDisplayFormatterUsesCurrentPhotographerAndAllUniqueStores() {
func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() {
XCTAssertEqual(
PaymentAccountDisplayFormatter.photographerName(userName: " 当前摄影师 ", realName: "实名"),
"当前摄影师"
PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "),
"摄影师真实姓名"
)
XCTAssertEqual(PaymentAccountDisplayFormatter.photographerName(realName: " "), "-")
XCTAssertEqual(
PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]),
"一号店、二号店"
@@ -26,6 +26,22 @@ final class ProfileViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.displayNickname, "未设置昵称")
}
func testColdStartBasicInfoUsesPersistedSessionCache() {
AppStore.shared.session.userName = "缓存昵称"
AppStore.shared.session.realName = "缓存真实姓名"
AppStore.shared.session.phone = "186****7230"
AppStore.shared.session.avatar = "https://cdn.example.com/cached-avatar.jpg"
let viewModel = ProfileViewModel()
XCTAssertTrue(viewModel.hasCachedBasicInfo)
XCTAssertEqual(viewModel.displayNickname, "缓存昵称")
XCTAssertEqual(viewModel.displayRealName, "缓存真实姓名")
XCTAssertEqual(viewModel.displayPhone, "186****7230")
XCTAssertEqual(viewModel.displayAvatarURL, "https://cdn.example.com/cached-avatar.jpg")
XCTAssertEqual(viewModel.displayUID, "profile-user")
}
func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
let viewModel = ProfileViewModel()
@@ -3,6 +3,7 @@
// suixinkanTests
//
import CoreLocation
import XCTest
@testable import suixinkan
@@ -0,0 +1,62 @@
//
// SessionExpiredDialogViewControllerTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
/// 登录失效弹窗测试,覆盖强制展示配置、Android 对齐文案与确认防重。
@MainActor
final class SessionExpiredDialogViewControllerTests: XCTestCase {
func testDialogUsesForcedPresentationAndAndroidCopy() throws {
let dialog = SessionExpiredDialogViewController(onConfirm: {})
dialog.loadViewIfNeeded()
XCTAssertEqual(dialog.modalPresentationStyle, .overFullScreen)
XCTAssertEqual(dialog.modalTransitionStyle, .crossDissolve)
XCTAssertTrue(dialog.isModalInPresentation)
XCTAssertTrue(dialog.view.accessibilityViewIsModal)
let labels = dialog.view.allSubviews(of: UILabel.self)
XCTAssertTrue(labels.contains { $0.text == "登录提醒" })
XCTAssertTrue(labels.contains { $0.text == "当前用户已在其他设备登录,\n请重新登录" })
let button = try XCTUnwrap(
dialog.view.allSubviews(of: UIButton.self).first {
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
}
)
XCTAssertEqual(button.title(for: .normal), "确认并退出登录")
}
func testConfirmCallbackOnlyRunsOnce() throws {
var confirmationCount = 0
let dialog = SessionExpiredDialogViewController {
confirmationCount += 1
}
dialog.loadViewIfNeeded()
let button = try XCTUnwrap(
dialog.view.allSubviews(of: UIButton.self).first {
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
}
)
button.sendActions(for: .touchUpInside)
button.sendActions(for: .touchUpInside)
XCTAssertEqual(confirmationCount, 1)
XCTAssertFalse(button.isEnabled)
}
}
private extension UIView {
func allSubviews<View: UIView>(of type: View.Type) -> [View] {
subviews.flatMap { subview -> [View] in
let current = (subview as? View).map { [$0] } ?? []
return current + subview.allSubviews(of: type)
}
}
}