优化 UI Test 路由直达与会话复用,提升自定义 TabBar 下的稳定性。
新增启动参数直达首页与个人中心页面,引入 AppUITestRouteDriver 与 UITestSession 共享登录,并重构各模块用例的导航断言逻辑。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -42,6 +42,9 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
|
||||
- `AppUITestLaunchState` 在收到 `-suixinkan-ui-tests` 时跳过推送注册和排队 WebSocket,避免系统弹窗干扰自动化。
|
||||
- `-suixinkan-ui-tests-reset-state` 用于冷启动清理 Keychain 与 UserDefaults。
|
||||
- `-suixinkan-ui-tests-open-menu <菜单标题>` 登录后直达首页调试目录中的目标页。
|
||||
- `-suixinkan-ui-tests-open-profile <路由名>` 登录后直达个人中心二级页(如 `settings`、`realNameAuth`)。
|
||||
- `AppUITestRouteDriver` 仅在 DEBUG 构建下解析上述直达参数,供 XCUITest 逐页验证。
|
||||
- 详细运行方式见 `suixinkanUITests/README.md`。
|
||||
|
||||
## 登录和退出
|
||||
|
||||
@ -13,12 +13,36 @@ enum AppUITestLaunchState {
|
||||
static let uiTestsArgument = "-suixinkan-ui-tests"
|
||||
/// 冷启动前清理本地登录缓存。
|
||||
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
||||
/// 登录后直达首页调试菜单项(下一参数为菜单标题)。
|
||||
static let openMenuArgument = "-suixinkan-ui-tests-open-menu"
|
||||
/// 登录后直达个人中心二级页(下一参数为路由名)。
|
||||
static let openProfileArgument = "-suixinkan-ui-tests-open-profile"
|
||||
|
||||
/// 当前进程是否由 XCUITest 启动。
|
||||
static var isRunningUITests: Bool {
|
||||
ProcessInfo.processInfo.arguments.contains(uiTestsArgument)
|
||||
}
|
||||
|
||||
/// 待打开的首页调试菜单标题。
|
||||
static var pendingMenuTitle: String? {
|
||||
argumentValue(following: openMenuArgument)
|
||||
}
|
||||
|
||||
/// 待打开的个人中心路由标识。
|
||||
static var pendingProfileRouteRawValue: String? {
|
||||
argumentValue(following: openProfileArgument)
|
||||
}
|
||||
|
||||
/// 读取启动参数中紧跟在指定 flag 后的值。
|
||||
private static func argumentValue(following flag: String) -> String? {
|
||||
let arguments = ProcessInfo.processInfo.arguments
|
||||
guard let index = arguments.firstIndex(of: flag), index + 1 < arguments.count else {
|
||||
return nil
|
||||
}
|
||||
let value = arguments[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
|
||||
/// 按需在冷启动时清理 token、账号快照和偏好设置。
|
||||
static func resetIfNeeded(arguments: [String] = ProcessInfo.processInfo.arguments) {
|
||||
guard arguments.contains(resetArgument) else { return }
|
||||
|
||||
86
suixinkan/App/AppUITestRouteDriver.swift
Normal file
86
suixinkan/App/AppUITestRouteDriver.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// AppUITestRouteDriver.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
/// UI Test 专用路由直达器,通过启动参数打开目标页,绕开自定义 TabBar 下 XCUITest 难以触发的 push。
|
||||
@MainActor
|
||||
enum AppUITestRouteDriver {
|
||||
private static var didApply = false
|
||||
|
||||
/// 根据启动参数跳转到指定首页菜单或个人中心页面。
|
||||
static func applyIfNeeded(appRouter: AppRouter) async {
|
||||
guard AppUITestLaunchState.isRunningUITests, !didApply else { return }
|
||||
guard AppUITestLaunchState.pendingMenuTitle != nil
|
||||
|| AppUITestLaunchState.pendingProfileRouteRawValue != nil else { return }
|
||||
|
||||
didApply = true
|
||||
try? await Task.sleep(nanoseconds: 600_000_000)
|
||||
|
||||
if let menuTitle = AppUITestLaunchState.pendingMenuTitle {
|
||||
openHomeMenu(title: menuTitle, appRouter: appRouter)
|
||||
return
|
||||
}
|
||||
|
||||
if let rawValue = AppUITestLaunchState.pendingProfileRouteRawValue {
|
||||
openProfileRoute(rawValue: rawValue, appRouter: appRouter)
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开首页调试目录中的指定菜单。
|
||||
private static func openHomeMenu(title: String, appRouter: AppRouter) {
|
||||
guard let item = HomeMenuRouter.debugAllMenuItems().first(where: { $0.title == title }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let resolved = HomeMenuRouter.resolve(uri: item.uri, title: item.title)
|
||||
switch resolved {
|
||||
case .tab(let tab):
|
||||
appRouter.select(tab)
|
||||
case .orders(let entry):
|
||||
appRouter.selectOrders(entry: entry)
|
||||
case .destination(let homeRoute):
|
||||
appRouter.select(.home)
|
||||
appRouter.router(for: .home).navigate(to: .home(homeRoute))
|
||||
case .unsupported(let uri, let title, _):
|
||||
appRouter.select(.home)
|
||||
appRouter.router(for: .home).navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
|
||||
case .placeholder(let uri, let title):
|
||||
appRouter.select(.home)
|
||||
appRouter.router(for: .home).navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开个人中心二级页面。
|
||||
private static func openProfileRoute(rawValue: String, appRouter: AppRouter) {
|
||||
let route: ProfileRoute?
|
||||
switch rawValue {
|
||||
case "settings":
|
||||
route = .settings
|
||||
case "realNameAuth":
|
||||
route = .realNameAuth
|
||||
case "accountSwitch":
|
||||
route = .accountSwitch
|
||||
case "debugHomeMenus":
|
||||
route = .debugHomeMenus
|
||||
case "agreement.about":
|
||||
route = .agreement(.about)
|
||||
case "agreement.userAgreement":
|
||||
route = .agreement(.userAgreement)
|
||||
case "agreement.privacyPolicy":
|
||||
route = .agreement(.privacyPolicy)
|
||||
default:
|
||||
route = nil
|
||||
}
|
||||
|
||||
guard let route else { return }
|
||||
appRouter.select(.profile)
|
||||
appRouter.router(for: .profile).navigate(to: .profile(route))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -212,6 +212,11 @@ struct RootView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
case .loggedIn:
|
||||
MainTabsView()
|
||||
.task {
|
||||
#if DEBUG
|
||||
await AppUITestRouteDriver.applyIfNeeded(appRouter: appRouter)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user