// // NavigationRouterTests.swift // suixinkanTests // // Created by Codex on 2026/6/22. // import UIKit import XCTest @testable import suixinkan_ios /// UIKit 主导航器测试,覆盖 Tab 宿主、全局入口和当前栈 push 行为。 @MainActor 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) } /// 测试 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) navigator.openHome(.taskManagement, policy: .tabRoot(.home), animated: false) let homeStack = host.navigationController(for: .home)! XCTAssertEqual(host.selectedTab, .home) XCTAssertEqual(homeStack.viewControllers.count, 2) XCTAssertTrue(homeStack.viewControllers.last is TaskManagementViewController) } /// 测试 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) navigator.openProfile(.realNameAuth, policy: .preserveTab(.profile), animated: false) 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) } /// 测试宿主未挂载时全局请求会排队,并在 attach 后按顺序执行。 func testPendingActionsRunAfterAttachInFIFOOrder() { let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession())) let host = AppNavigatorTestHost(services: services) let navigator = AppNavigator() 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) } /// 测试 resetAllStacks 会清空暂存请求并重置已挂载宿主。 func testResetAllStacksClearsPendingAndMountedStacks() { let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession())) let host = AppNavigatorTestHost(services: services) let navigator = AppNavigator() navigator.openHome(.paymentCollection, policy: .tabRoot(.home), animated: false) navigator.resetAllStacks() navigator.attach(host: host) XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 1) navigator.openHome(.wallet, policy: .tabRoot(.home), animated: false) navigator.resetAllStacks() XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 1) XCTAssertEqual(host.selectedTab, .home) } /// 测试打开订单入口会切到订单 Tab、回到根页面并传递扫码结果。 func testOpenOrdersEntryAppliesRootStateAndScanCode() { let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession())) let host = AppNavigatorTestHost(services: services) let navigator = AppNavigator() navigator.attach(host: host) navigator.openOrdersEntry(.verificationOrders, scannedCode: "VERIFY001") 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) } }