Merge branch 'dev_1_2_1' into xh_test
This commit is contained in:
@@ -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, "门店真实姓名")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user