// // 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() { let expectations: [(String, HomeMenuResolvedRoute)] = [ ("space_settings", .destination(.profileSpaceSettings)), ("scenicselection", .destination(.scenicSelection)), ("store", .destination(.moreFunctions)), ("wallet", .destination(.wallet)), ("payment_collection", .destination(.paymentCollection)), ("payment_qr", .destination(.paymentCollection)), ("payment_code", .destination(.paymentCollection)), ("permission_apply", .destination(.permissionApply)), ("permission_apply_status", .destination(.permissionApplyStatus)), ("scenicapplication", .destination(.scenicApplication)), ("task_management", .destination(.taskManagement)), ("task_management_editor", .destination(.taskManagement)), ("task_create", .destination(.taskCreate)), ("cloud_management", .destination(.cloudStorage)), ("cloud_storage_transit", .destination(.cloudStorageTransit)), ("asset_management", .destination(.materialLibrary)), ("material_upload", .destination(.materialUpload)), ("album_list", .destination(.albumList)), ("album_trailer", .destination(.albumTrailer)), ("sample_management", .destination(.sampleLibrary)), ("sample_upload", .destination(.sampleUpload)), ("checkin_points", .destination(.punchPointList)), ("location_report", .destination(.locationReport)), ("location_report_history", .destination(.locationReportHistory)), ("photographer_report", .destination(.photographerReport)), ("pm", .destination(.projectManagement)), ("project_edit", .destination(.projectManagement)), ("pm_manager", .destination(.pmProjectManagement)), ("schedule_management", .destination(.scheduleManagement)), ("registration_invitation", .destination(.photographerInvite)), ("photographer_invite", .destination(.photographerInvite)), ("invite_record", .destination(.inviteRecord)), ("deposit_order_detail", .destination(.depositOrders)), ("deposit_order", .destination(.depositOrders)), ("deposit_order_shooting_info", .destination(.depositOrders)), ("withdrawal_audit", .destination(.withdrawalAudit)), ("cooperation_order", .destination(.cooperationOrder)), ("scenic_settlement", .destination(.scenicSettlement)), ("scenic_settlement_review", .destination(.scenicSettlementReview)), ("message_center", .destination(.messageCenter)), ("/scenic-queue", .destination(.queueManagement)), ("queue_management", .destination(.queueManagement)), ("live_stream_management", .destination(.liveManagement)), ("live_album", .destination(.liveAlbum)), ("travel_album", .destination(.travelAlbumEntry)), ("operating-area", .destination(.operatingArea)), ("pilot_cert", .destination(.pilotCertification)) ] for (uri, expected) in expectations { XCTAssertEqual(HomeMenuRouter.resolve(uri: uri, title: ""), expected, "URI \(uri) should resolve to migrated destination") } } /// 测试 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: ""), .orderPush(.writeOffList)) 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( roleCode: "photographer", 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, currentRoleCode: "photographer") 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("photographer_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: "photographer_report", title: ""), .destination(.photographerReport)) 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)) XCTAssertEqual(HomeMenuRouter.resolve(uri: "travel_album", title: ""), .destination(.travelAlbumEntry)) } #endif /// 创建角色权限测试实体。 private func rolePermission(roleCode: String, roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse { RolePermissionResponse( role: RoleInfo(id: roleId, name: "角色\(roleId)", roleCode: roleCode, permission: permissions), scenic: [] ) } /// 创建权限节点测试实体。 private func permission(id: Int, name: String, uri: String, children: [PermissionItem] = []) -> PermissionItem { PermissionItem(id: id, name: name, uri: uri, children: children) } }