// // HomeMenuRouterTests.swift // suixinkanTests // // Created by Codex on 2026/6/22. // import XCTest @testable import suixinkan /// 首页菜单路由测试,覆盖 URI 映射、别名和未知路由诊断。 final class HomeMenuRouterTests: XCTestCase { override func setUp() { super.setUp() HomeRouteDiagnostics.resetUnknownRoutes() } override func tearDown() { HomeRouteDiagnostics.resetUnknownRoutes() super.tearDown() } /// 测试已迁移的首页路由会解析到真实目标。 func testMigratedRoutesResolveToDestinations() { XCTAssertEqual(HomeMenuRouter.resolve(uri: "space_settings", title: ""), .destination(.profileSpace)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenicselection", title: ""), .destination(.scenicSelection)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "store", title: ""), .destination(.moreFunctions)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "wallet", title: ""), .destination(.wallet)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "payment_collection", title: ""), .destination(.paymentCollection)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "payment_qr", title: ""), .destination(.paymentCollection)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "payment_code", title: ""), .destination(.paymentCollection)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "permission_apply", title: ""), .destination(.permissionApply)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "permission_apply_status", title: ""), .destination(.permissionApplyStatus)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenicapplication", title: ""), .destination(.scenicApplication)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_management", title: ""), .destination(.taskManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_management_editor", title: ""), .destination(.taskManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "task_create", title: ""), .destination(.taskCreate)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "cloud_management", title: ""), .destination(.cloudStorage)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "cloud_storage_transit", title: ""), .destination(.cloudStorageTransit)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "asset_management", title: ""), .destination(.materialLibrary)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "material_upload", title: ""), .destination(.materialUpload)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "album_list", title: ""), .destination(.albumList)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "album_trailer", title: ""), .destination(.albumTrailer)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "sample_management", title: ""), .destination(.sampleLibrary)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "sample_upload", title: ""), .destination(.sampleUpload)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "checkin_points", title: ""), .destination(.punchPointList)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "location_report", title: ""), .destination(.locationReport)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "location_report_history", title: ""), .destination(.locationReportHistory)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm", title: ""), .destination(.projectManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "project_edit", title: ""), .destination(.projectManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm_manager", title: ""), .destination(.pmProjectManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "schedule_management", title: ""), .destination(.scheduleManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "registration_invitation", title: ""), .destination(.photographerInvite)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_invite", title: ""), .destination(.photographerInvite)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "invite_record", title: ""), .destination(.inviteRecord)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order_detail", title: ""), .destination(.depositOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order", title: ""), .destination(.depositOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order_shooting_info", title: ""), .destination(.depositOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "withdrawal_audit", title: ""), .destination(.withdrawalAudit)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenic_settlement", title: ""), .destination(.scenicSettlement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenic_settlement_review", title: ""), .destination(.scenicSettlementReview)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "message_center", title: ""), .destination(.messageCenter)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "/scenic-queue", title: ""), .destination(.queueManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "queue_management", title: ""), .destination(.queueManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "live_stream_management", title: ""), .destination(.liveManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "live_album", title: ""), .destination(.liveAlbum)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "operating-area", title: ""), .destination(.operatingArea)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "pilot_cert", title: ""), .destination(.pilotCertification)) } /// 测试 Tab 路由仍集中在 HomeMenuRouter。 func testTabRoutesStayCentralized() { XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_orders", title: ""), .orders(.storeOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "/scenic-order-manage", title: ""), .orders(.storeOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "verification_order", title: ""), .orders(.verificationOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_stats", title: ""), .tab(.statistics)) } /// 测试飞控相关入口属于已知不支持范围。 func testFlightControlRoutesAreKnownUnsupportedScope() { XCTAssertEqual( HomeMenuRouter.resolve(uri: "fly", title: ""), .unsupported(uri: "fly", title: "飞行管理", reason: "DJI/飞控模块已明确不纳入 iOS 迁移和后续开发范围。") ) } /// 测试未知 URI 会显式进入 placeholder。 func testUnknownRouteIsExplicitPlaceholder() { XCTAssertEqual( HomeMenuRouter.resolve(uri: "android_only_feature", title: "Android 专项"), .placeholder(uri: "android_only_feature", title: "Android 专项") ) } /// 测试 canonicalURI 会使用当前权限可用的同义 URI。 func testCanonicalURIUsesAvailableAlias() { let available: Set = ["task_management_editor", "photographer_invite", "project_edit", "pm_manager", "payment_qr"] XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "task_management", availableURIs: available), "task_management_editor") XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "registration_invitation", availableURIs: available), "photographer_invite") XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "pm", availableURIs: available), "project_edit") XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "pm_manager", availableURIs: available), "pm_manager") XCTAssertEqual(HomeMenuRouter.canonicalURI(for: "payment_code", availableURIs: available), "payment_qr") } /// 测试 menuAliasKey 会把同义入口分到同一组。 func testMenuAliasKeyGroupsDuplicateAndroidEntries() { XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "task_management_editor"), "task_management") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "photographer_invite"), "registration_invitation") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "project_edit"), "pm") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "pm_manager"), "pm_manager") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "payment_code"), "payment_collection") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "deposit_order_shooting_info"), "deposit_order") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "/scenic-order-manage"), "photographer_orders") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "/scenic-queue"), "queue_management") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "queue_management"), "queue_management") XCTAssertEqual(HomeMenuRouter.menuAliasKey(for: "android_only_module"), "android_only_module") } /// 测试未知路由诊断会去重并更新标题。 func testUnknownRouteDiagnosticsDeduplicatesAndUpdatesTitle() { HomeRouteDiagnostics.recordUnknown(uri: "android_feature", title: "旧标题") HomeRouteDiagnostics.recordUnknown(uri: "android_feature", title: "新标题") HomeRouteDiagnostics.recordUnknown(uri: "another_feature", title: "") let records = HomeRouteDiagnostics.unknownRoutes() XCTAssertEqual(records.count, 2) XCTAssertEqual(records.first { $0.uri == "android_feature" }?.title, "新标题") XCTAssertEqual(records.first { $0.uri == "android_feature" }?.count, 2) XCTAssertEqual(records.first { $0.uri == "another_feature" }?.count, 1) } /// 测试未知路由诊断报告是 Markdown 且按出现次数排序。 func testUnknownRouteDiagnosticsReportIsMarkdownAndSortedByCount() { HomeRouteDiagnostics.recordUnknown(uri: "rare_feature", title: "低频入口") HomeRouteDiagnostics.recordUnknown(uri: "hot_feature", title: "高频入口") HomeRouteDiagnostics.recordUnknown(uri: "hot_feature", title: "高频入口") let report = HomeRouteDiagnostics.unknownRouteReport(generatedAt: Date(timeIntervalSince1970: 0)) XCTAssertTrue(report.contains("# Home Route Diagnostics")) XCTAssertTrue(report.contains("| hot_feature | 高频入口 | 2 |")) XCTAssertLessThan( report.range(of: "hot_feature")!.lowerBound, report.range(of: "rare_feature")!.lowerBound ) } /// 测试权限路由审计会区分可路由、不支持和未知入口。 func testPermissionRouteAuditSeparatesRoutableUnsupportedAndUnknownEntries() { let permissions = [ rolePermission( roleId: 1, permissions: [ permission(id: 1, name: "任务管理", uri: "task_management", children: [ permission(id: 2, name: "发布任务", uri: "task_create") ]), permission(id: 3, name: "飞控", uri: "pilot_controller"), permission(id: 4, name: "Android 专项入口", uri: "android_only_module"), permission(id: 5, name: "重复任务", uri: "task_management") ] ) ] let audit = HomePermissionRouteAuditor.audit(permissions: permissions, currentRoleId: 1) XCTAssertEqual(audit.routable.map(\.uri), ["task_management", "task_create"]) XCTAssertEqual(audit.unsupported.map(\.uri), ["pilot_controller"]) XCTAssertEqual(audit.unknown.map(\.uri), ["android_only_module"]) XCTAssertTrue(audit.hasUnknownRoutes) } #if DEBUG /// 测试 DEBUG 菜单列表不依赖权限,包含已迁移和未迁移入口。 func testDebugAllMenuItemsExposeKnownMenusWithoutPermissions() { let items = HomeMenuRouter.debugAllMenuItems() let uris = items.map(\.uri) XCTAssertTrue(uris.contains("album_list")) XCTAssertTrue(uris.contains("wallet")) XCTAssertTrue(uris.contains("sample_management")) XCTAssertTrue(uris.contains("checkin_points")) XCTAssertTrue(uris.contains("location_report")) XCTAssertTrue(uris.contains("pm")) XCTAssertTrue(uris.contains("pm_manager")) XCTAssertTrue(uris.contains("schedule_management")) XCTAssertTrue(uris.contains("registration_invitation")) XCTAssertTrue(uris.contains("invite_record")) XCTAssertTrue(uris.contains("deposit_order")) XCTAssertTrue(uris.contains("deposit_order_shooting_info")) XCTAssertTrue(uris.contains("message_center")) XCTAssertTrue(uris.contains("/scenic-queue")) XCTAssertTrue(uris.contains("queue_management")) XCTAssertTrue(uris.contains("live_stream_management")) XCTAssertTrue(uris.contains("live_album")) XCTAssertEqual(HomeMenuRouter.resolve(uri: "album_list", title: ""), .destination(.albumList)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "sample_management", title: ""), .destination(.sampleLibrary)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "sample_upload", title: ""), .destination(.sampleUpload)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "checkin_points", title: ""), .destination(.punchPointList)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "location_report", title: ""), .destination(.locationReport)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm", title: ""), .destination(.projectManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm_manager", title: ""), .destination(.pmProjectManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "schedule_management", title: ""), .destination(.scheduleManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "registration_invitation", title: ""), .destination(.photographerInvite)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "invite_record", title: ""), .destination(.inviteRecord)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order", title: ""), .destination(.depositOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order_shooting_info", title: ""), .destination(.depositOrders)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "message_center", title: ""), .destination(.messageCenter)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "/scenic-queue", title: ""), .destination(.queueManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "queue_management", title: ""), .destination(.queueManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "live_stream_management", title: ""), .destination(.liveManagement)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "live_album", title: ""), .destination(.liveAlbum)) } #endif /// 创建角色权限测试实体。 private func rolePermission(roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse { RolePermissionResponse( role: RoleInfo(id: roleId, name: "角色\(roleId)", permission: permissions), scenic: [] ) } /// 创建权限节点测试实体。 private func permission(id: Int, name: String, uri: String, children: [PermissionItem] = []) -> PermissionItem { PermissionItem(id: id, name: name, uri: uri, children: children) } }