Integrate CYLTabBar center scan button and unify UIKit navigation.
Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -5,93 +5,167 @@
|
||||
// Created by Codex on 2026/6/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan_ios
|
||||
|
||||
/// 主导航路由测试,覆盖 AppRoute 的跨页面导航策略。
|
||||
/// UIKit 主导航器测试,覆盖 Tab 宿主、全局入口和当前栈 push 行为。
|
||||
@MainActor
|
||||
final class NavigationRouterTests: XCTestCase {
|
||||
/// 测试通过 NavigationStack push 的子页面默认隐藏底部 TabBar。
|
||||
func testPushedRoutesHideTabBarByDefault() {
|
||||
XCTAssertTrue(AppRoute.placeholder(title: "详情").hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.home(.moreFunctions).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.home(.paymentCollection).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.home(.wallet).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.orders(.storeDetail(try! Self.firstOrder())).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.orders(.writeOffDetail(try! Self.firstWriteOffOrder())).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.orders(.multiTravelTaskUpload(orderNumber: "MT001")).hidesTabBarWhenPushed)
|
||||
XCTAssertTrue(AppRoute.orders(.orderTrailer(orderNumber: "MT001", title: "尾片上传")).hidesTabBarWhenPushed)
|
||||
final class AppNavigatorTests: XCTestCase {
|
||||
/// 测试当前栈 push 会增加选中导航栈层级,并隐藏底部 TabBar。
|
||||
func testPushUsesCurrentNavigationStack() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
navigator.attach(host: host)
|
||||
|
||||
navigator.push(.home(.paymentCollection), from: nil, animated: false)
|
||||
|
||||
let homeStack = host.navigationController(for: .home)!
|
||||
XCTAssertEqual(homeStack.viewControllers.count, 2)
|
||||
XCTAssertTrue(homeStack.viewControllers.last?.hidesBottomBarWhenPushed == true)
|
||||
XCTAssertTrue(homeStack.viewControllers.last is PaymentCollectionViewController)
|
||||
}
|
||||
|
||||
/// 测试切换订单 Tab 时会保存订单内部子入口。
|
||||
func testSelectOrdersUpdatesTabAndEntry() {
|
||||
let router = AppRouter()
|
||||
/// 测试 tabRoot 会切换到指定 Tab、清空旧栈并打开目标页。
|
||||
func testTabRootSelectsTabResetsStackAndPushesRoute() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
navigator.attach(host: host)
|
||||
host.navigationController(for: .home)?.push(route: .home(.wallet), animated: false)
|
||||
|
||||
router.selectOrders(entry: .verificationOrders)
|
||||
navigator.openHome(.taskManagement, policy: .tabRoot(.home), animated: false)
|
||||
|
||||
XCTAssertEqual(router.selectedTab, .orders)
|
||||
XCTAssertEqual(router.selectedOrdersEntry, .verificationOrders)
|
||||
let homeStack = host.navigationController(for: .home)!
|
||||
XCTAssertEqual(host.selectedTab, .home)
|
||||
XCTAssertEqual(homeStack.viewControllers.count, 2)
|
||||
XCTAssertTrue(homeStack.viewControllers.last is TaskManagementViewController)
|
||||
}
|
||||
|
||||
/// 测试全局扫码核销路由会切到订单核销入口并保存扫码结果。
|
||||
func testRouteToOrderVerificationStoresPendingScanCode() {
|
||||
let router = AppRouter()
|
||||
/// 测试 preserveTab 只切换 Tab 并保留旧栈。
|
||||
func testPreserveTabKeepsExistingStack() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
navigator.attach(host: host)
|
||||
host.navigationController(for: .profile)?.push(route: .profile(.settings), animated: false)
|
||||
|
||||
router.routeToOrderVerification(scannedCode: "order_number=VERIFY001")
|
||||
navigator.openProfile(.realNameAuth, policy: .preserveTab(.profile), animated: false)
|
||||
|
||||
XCTAssertEqual(router.selectedTab, .orders)
|
||||
XCTAssertEqual(router.selectedOrdersEntry, .verificationOrders)
|
||||
XCTAssertEqual(router.pendingOrderScanCode, "order_number=VERIFY001")
|
||||
let profileStack = host.navigationController(for: .profile)!
|
||||
XCTAssertEqual(host.selectedTab, .profile)
|
||||
XCTAssertEqual(profileStack.viewControllers.count, 3)
|
||||
XCTAssertTrue(profileStack.viewControllers[1] is SettingsViewController)
|
||||
XCTAssertTrue(profileStack.viewControllers.last is RealNameAuthViewController)
|
||||
}
|
||||
|
||||
/// 测试全局扫码结果只能被消费一次。
|
||||
func testConsumePendingOrderScanCodeClearsValueAfterFirstRead() {
|
||||
let router = AppRouter()
|
||||
router.routeToOrderVerification(scannedCode: "VERIFY001")
|
||||
/// 测试宿主未挂载时全局请求会排队,并在 attach 后按顺序执行。
|
||||
func testPendingActionsRunAfterAttachInFIFOOrder() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
|
||||
XCTAssertEqual(router.consumePendingOrderScanCode(), "VERIFY001")
|
||||
XCTAssertNil(router.consumePendingOrderScanCode())
|
||||
navigator.openHome(.paymentCollection, policy: .tabRoot(.home), animated: false)
|
||||
navigator.openHome(.messageCenter, policy: .preserveTab(.home), animated: false)
|
||||
navigator.attach(host: host)
|
||||
|
||||
let homeStack = host.navigationController(for: .home)!
|
||||
XCTAssertEqual(homeStack.viewControllers.count, 3)
|
||||
XCTAssertTrue(homeStack.viewControllers[1] is PaymentCollectionViewController)
|
||||
XCTAssertTrue(homeStack.viewControllers.last is MessageCenterViewController)
|
||||
}
|
||||
|
||||
/// 测试重置主路由时会清理未消费的扫码结果。
|
||||
func testResetClearsPendingOrderScanCode() {
|
||||
let router = AppRouter()
|
||||
router.routeToOrderVerification(scannedCode: "VERIFY001")
|
||||
/// 测试 resetAllStacks 会清空暂存请求并重置已挂载宿主。
|
||||
func testResetAllStacksClearsPendingAndMountedStacks() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
|
||||
router.reset()
|
||||
navigator.openHome(.paymentCollection, policy: .tabRoot(.home), animated: false)
|
||||
navigator.resetAllStacks()
|
||||
navigator.attach(host: host)
|
||||
XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 1)
|
||||
|
||||
XCTAssertEqual(router.selectedTab, .home)
|
||||
XCTAssertEqual(router.selectedOrdersEntry, .storeOrders)
|
||||
XCTAssertNil(router.pendingOrderScanCode)
|
||||
navigator.openHome(.wallet, policy: .tabRoot(.home), animated: false)
|
||||
navigator.resetAllStacks()
|
||||
XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 1)
|
||||
XCTAssertEqual(host.selectedTab, .home)
|
||||
}
|
||||
|
||||
/// 测试订单模块真实路由可被压入导航栈。
|
||||
func testOrderRoutesCanBePushedIntoRouterPath() throws {
|
||||
let router = RouterPath()
|
||||
let order = try Self.firstOrder()
|
||||
let writeOff = try Self.firstWriteOffOrder()
|
||||
/// 测试打开订单入口会切到订单 Tab、回到根页面并传递扫码结果。
|
||||
func testOpenOrdersEntryAppliesRootStateAndScanCode() {
|
||||
let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))
|
||||
let host = AppNavigatorTestHost(services: services)
|
||||
let navigator = AppNavigator()
|
||||
navigator.attach(host: host)
|
||||
|
||||
router.navigate(to: .orders(.storeDetail(order)))
|
||||
router.navigate(to: .orders(.writeOffDetail(writeOff)))
|
||||
router.navigate(to: .orders(.multiTravelTaskUpload(orderNumber: "MT001")))
|
||||
router.navigate(to: .orders(.orderTrailer(orderNumber: "MT001", title: "视频预告")))
|
||||
navigator.openOrdersEntry(.verificationOrders, scannedCode: "VERIFY001")
|
||||
|
||||
XCTAssertEqual(router.path.count, 4)
|
||||
XCTAssertEqual(router.path[0], .orders(.storeDetail(order)))
|
||||
XCTAssertEqual(router.path[1], .orders(.writeOffDetail(writeOff)))
|
||||
XCTAssertEqual(router.path[2], .orders(.multiTravelTaskUpload(orderNumber: "MT001")))
|
||||
XCTAssertEqual(router.path[3], .orders(.orderTrailer(orderNumber: "MT001", title: "视频预告")))
|
||||
}
|
||||
|
||||
/// 返回测试用订单管理列表项。
|
||||
private static func firstOrder() throws -> OrderEntity {
|
||||
let payload = try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")
|
||||
return try XCTUnwrap(payload.list.first)
|
||||
}
|
||||
|
||||
/// 返回测试用核销订单列表项。
|
||||
private static func firstWriteOffOrder() throws -> WriteOffOrderItem {
|
||||
let payload = try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")
|
||||
return try XCTUnwrap(payload.list.first)
|
||||
XCTAssertEqual(host.selectedTab, .orders)
|
||||
XCTAssertEqual(host.appliedOrdersEntry, .verificationOrders)
|
||||
XCTAssertEqual(host.appliedScanCode, "VERIFY001")
|
||||
XCTAssertEqual(host.navigationController(for: .orders)?.viewControllers.count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// AppNavigator 单测专用宿主,模拟主 Tab 容器能力。
|
||||
@MainActor
|
||||
private final class AppNavigatorTestHost: AppNavigationHost {
|
||||
private let services: AppServices
|
||||
private var controllers: [AppTab: TabNavigationController] = [:]
|
||||
var selectedTab: AppTab = .home
|
||||
var appliedOrdersEntry: OrdersEntry?
|
||||
var appliedScanCode: String?
|
||||
|
||||
/// 初始化所有 Tab 导航栈。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
for tab in AppTab.allCases {
|
||||
controllers[tab] = TabNavigationController(tab: tab, services: services)
|
||||
}
|
||||
}
|
||||
|
||||
/// 选中指定主 Tab。
|
||||
func selectTab(_ tab: AppTab) {
|
||||
selectedTab = tab
|
||||
}
|
||||
|
||||
/// 返回指定 Tab 的导航控制器。
|
||||
func navigationController(for tab: AppTab) -> TabNavigationController? {
|
||||
controllers[tab]
|
||||
}
|
||||
|
||||
/// 返回当前选中 Tab 的导航控制器。
|
||||
func selectedNavigationController() -> TabNavigationController? {
|
||||
controllers[selectedTab]
|
||||
}
|
||||
|
||||
/// 重置全部 Tab 导航栈。
|
||||
func resetAllStacks() {
|
||||
for controller in controllers.values {
|
||||
controller.popToRootViewController(animated: false)
|
||||
}
|
||||
selectedTab = .home
|
||||
}
|
||||
|
||||
/// 记录订单根页面接收到的入口状态。
|
||||
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) {
|
||||
appliedOrdersEntry = entry
|
||||
appliedScanCode = scannedCode
|
||||
}
|
||||
}
|
||||
|
||||
/// 空网络会话,避免路由测试发起真实请求。
|
||||
private final class AppNavigatorRecordingURLSession: URLSessionProtocol {
|
||||
/// 返回空成功响应。
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
let response = HTTPURLResponse(
|
||||
url: request.url ?? URL(string: "https://example.com")!,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: nil
|
||||
)!
|
||||
return (Data(), response)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user