diff --git a/suixinkan/App/App.md b/suixinkan/App/App.md index ff23698..e43201c 100644 --- a/suixinkan/App/App.md +++ b/suixinkan/App/App.md @@ -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`。 ## 登录和退出 diff --git a/suixinkan/App/AppUITestLaunchState.swift b/suixinkan/App/AppUITestLaunchState.swift index b62d170..a8d3dc2 100644 --- a/suixinkan/App/AppUITestLaunchState.swift +++ b/suixinkan/App/AppUITestLaunchState.swift @@ -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 } diff --git a/suixinkan/App/AppUITestRouteDriver.swift b/suixinkan/App/AppUITestRouteDriver.swift new file mode 100644 index 0000000..5ae8434 --- /dev/null +++ b/suixinkan/App/AppUITestRouteDriver.swift @@ -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 diff --git a/suixinkan/App/RootView.swift b/suixinkan/App/RootView.swift index 8bbb4b4..5f687c4 100644 --- a/suixinkan/App/RootView.swift +++ b/suixinkan/App/RootView.swift @@ -212,6 +212,11 @@ struct RootView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) case .loggedIn: MainTabsView() + .task { + #if DEBUG + await AppUITestRouteDriver.applyIfNeeded(appRouter: appRouter) + #endif + } } } diff --git a/suixinkanUITests/Auth/LoginSmokeUITests.swift b/suixinkanUITests/Auth/LoginSmokeUITests.swift index 3041c09..1755939 100644 --- a/suixinkanUITests/Auth/LoginSmokeUITests.swift +++ b/suixinkanUITests/Auth/LoginSmokeUITests.swift @@ -6,6 +6,16 @@ import XCTest final class LoginSmokeUITests: XCTestCase { + override class func setUp() { + super.setUp() + UITestSession.invalidate() + } + + override class func tearDown() { + UITestSession.invalidate() + super.tearDown() + } + override func setUp() { super.setUp() continueAfterFailure = false diff --git a/suixinkanUITests/Home/HomeRoutesUITests.swift b/suixinkanUITests/Home/HomeRoutesUITests.swift index c4997a9..0d35041 100644 --- a/suixinkanUITests/Home/HomeRoutesUITests.swift +++ b/suixinkanUITests/Home/HomeRoutesUITests.swift @@ -5,29 +5,58 @@ import XCTest +/// 首页调试菜单路由覆盖,每条菜单对应一个独立 test 方法,便于逐个执行与定位失败。 final class HomeRoutesUITests: AuthenticatedUITestCase { - /// 参数化遍历首页调试菜单,覆盖去重后的全部 Home 路由目标页。 - func testAllHomeRoutesFromDebugMenu() { - for entry in UITestHomeMenuCatalog.entries { - XCTContext.runActivity(named: entry.menuTitle) { _ in - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: entry.menuTitle, app: app) - UITestExpectationVerifier.verify(entry.expectation, app: app) - restoreAfterRoute(entry.expectation) - } - } - } + func testHomeRoute_空间设置() throws { try run(entryNamed: "空间设置") } + func testHomeRoute_相册管理() throws { try run(entryNamed: "相册管理") } + func testHomeRoute_相册预览上传() throws { try run(entryNamed: "相册预览上传") } + func testHomeRoute_我的钱包() throws { try run(entryNamed: "我的钱包") } + func testHomeRoute_立即收款() throws { try run(entryNamed: "立即收款") } + func testHomeRoute_相册云盘() throws { try run(entryNamed: "相册云盘") } + func testHomeRoute_传输管理() throws { try run(entryNamed: "传输管理") } + func testHomeRoute_素材管理() throws { try run(entryNamed: "素材管理") } + func testHomeRoute_上传素材() throws { try run(entryNamed: "上传素材") } + func testHomeRoute_任务管理() throws { try run(entryNamed: "任务管理") } + func testHomeRoute_发布任务() throws { try run(entryNamed: "发布任务") } + func testHomeRoute_日程管理() throws { try run(entryNamed: "日程管理") } + func testHomeRoute_消息中心() throws { try run(entryNamed: "消息中心") } + func testHomeRoute_打卡点管理() throws { try run(entryNamed: "打卡点管理") } + func testHomeRoute_样片管理() throws { try run(entryNamed: "样片管理") } + func testHomeRoute_上传样片() throws { try run(entryNamed: "上传样片") } + func testHomeRoute_直播管理() throws { try run(entryNamed: "直播管理") } + func testHomeRoute_直播相册() throws { try run(entryNamed: "直播相册") } + func testHomeRoute_景区选择() throws { try run(entryNamed: "景区选择") } + func testHomeRoute_景区申请() throws { try run(entryNamed: "景区申请") } + func testHomeRoute_权限申请() throws { try run(entryNamed: "权限申请") } + func testHomeRoute_权限申请状态() throws { try run(entryNamed: "权限申请状态") } + func testHomeRoute_景区结算() throws { try run(entryNamed: "景区结算") } + func testHomeRoute_结算审核() throws { try run(entryNamed: "结算审核") } + func testHomeRoute_核销订单() throws { try run(entryNamed: "核销订单") } + func testHomeRoute_景区订单() throws { try run(entryNamed: "景区订单") } + func testHomeRoute_数据统计() throws { try run(entryNamed: "数据统计") } + func testHomeRoute_项目管理() throws { try run(entryNamed: "项目管理") } + func testHomeRoute_店铺项目管理() throws { try run(entryNamed: "店铺项目管理") } + func testHomeRoute_位置上报() throws { try run(entryNamed: "位置上报") } + func testHomeRoute_注册邀请() throws { try run(entryNamed: "注册邀请") } + func testHomeRoute_邀请记录() throws { try run(entryNamed: "邀请记录") } + func testHomeRoute_店铺管理() throws { try run(entryNamed: "店铺管理") } + func testHomeRoute_飞行管理() throws { try run(entryNamed: "飞行管理") } + func testHomeRoute_飞手认证() throws { try run(entryNamed: "飞手认证") } + func testHomeRoute_飞控() throws { try run(entryNamed: "飞控") } + func testHomeRoute_排队管理() throws { try run(entryNamed: "排队管理") } + func testHomeRoute_运营区域() throws { try run(entryNamed: "运营区域") } + func testHomeRoute_押金订单详情() throws { try run(entryNamed: "押金订单详情") } + func testHomeRoute_提现审核() throws { try run(entryNamed: "提现审核") } + func testHomeRoute_定位上报历史() throws { try run(entryNamed: "定位上报历史") } + func testHomeRoute_更多功能() throws { try run(entryNamed: "更多功能") } + func testHomeRoute_系统设置() throws { try run(entryNamed: "系统设置") } - /// 根据打开结果恢复到调试菜单,便于继续下一项。 - private func restoreAfterRoute(_ expectation: UITestHomeMenuCatalog.Expectation) { - switch expectation { - case .tab: - DebugMenuNavigator.open(app: app) - case .staticText: - BackNavigator.dismissCustomTopBarIfNeeded(app: app) - DebugMenuNavigator.returnToMenu(app: app) - default: - DebugMenuNavigator.returnToMenu(app: app) + /// 按菜单标题执行单条首页路由测试。 + private func run(entryNamed menuTitle: String) throws { + guard let entry = UITestHomeMenuCatalog.entries.first(where: { $0.menuTitle == menuTitle }) else { + XCTFail("未在目录中找到菜单:\(menuTitle)") + return } + app = try HomeRouteTestRunner.verify(entry: entry) } } diff --git a/suixinkanUITests/Modules/InviteUITests.swift b/suixinkanUITests/Modules/InviteUITests.swift index 8e74475..68b75d5 100644 --- a/suixinkanUITests/Modules/InviteUITests.swift +++ b/suixinkanUITests/Modules/InviteUITests.swift @@ -7,9 +7,8 @@ import XCTest final class InviteUITests: AuthenticatedUITestCase { /// 测试邀请页与邀请记录页。 - func testInviteAndRecordPages() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "注册邀请", app: app) + func testInviteAndRecordPages() throws { + app = try UITestSession.relaunch(openMenu: "注册邀请") NavigationAssertions.waitForNavigationTitle("摄影师邀请", in: app) let recordEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '邀请记录'")).firstMatch @@ -17,9 +16,7 @@ final class InviteUITests: AuthenticatedUITestCase { recordEntry.tap() NavigationAssertions.waitForNavigationTitle("邀请记录", in: app) } else { - BackNavigator.goBack(app: app) - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "邀请记录", app: app) + app = try UITestSession.relaunch(openMenu: "邀请记录") NavigationAssertions.waitForNavigationTitle("邀请记录", in: app) } } diff --git a/suixinkanUITests/Modules/LiveUITests.swift b/suixinkanUITests/Modules/LiveUITests.swift index e8f3ae1..d3d34a8 100644 --- a/suixinkanUITests/Modules/LiveUITests.swift +++ b/suixinkanUITests/Modules/LiveUITests.swift @@ -7,9 +7,8 @@ import XCTest final class LiveUITests: AuthenticatedUITestCase { /// 测试直播管理列表与创建页。 - func testLiveManagementAndCreatePage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "直播管理", app: app) + func testLiveManagementAndCreatePage() throws { + app = try UITestSession.relaunch(openMenu: "直播管理") NavigationAssertions.waitForNavigationTitle("直播管理", in: app) let addButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '添加'")).firstMatch @@ -21,9 +20,8 @@ final class LiveUITests: AuthenticatedUITestCase { } /// 测试直播相册页可打开。 - func testLiveAlbumPage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "直播相册", app: app) + func testLiveAlbumPage() throws { + app = try UITestSession.relaunch(openMenu: "直播相册") NavigationAssertions.waitForNavigationTitle("直播相册", in: app) } } diff --git a/suixinkanUITests/Modules/LocationReportUITests.swift b/suixinkanUITests/Modules/LocationReportUITests.swift index 7e8ee6a..1de4331 100644 --- a/suixinkanUITests/Modules/LocationReportUITests.swift +++ b/suixinkanUITests/Modules/LocationReportUITests.swift @@ -7,9 +7,8 @@ import XCTest final class LocationReportUITests: AuthenticatedUITestCase { /// 测试位置上报与历史记录页。 - func testLocationReportAndHistory() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "位置上报", app: app) + func testLocationReportAndHistory() throws { + app = try UITestSession.relaunch(openMenu: "位置上报") NavigationAssertions.waitForNavigationTitle("位置上报", in: app) let historyEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '历史'")).firstMatch @@ -17,9 +16,7 @@ final class LocationReportUITests: AuthenticatedUITestCase { historyEntry.tap() NavigationAssertions.waitForNavigationTitle("位置上报历史", in: app) } else { - BackNavigator.goBack(app: app) - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "定位上报历史", app: app) + app = try UITestSession.relaunch(openMenu: "定位上报历史") NavigationAssertions.waitForNavigationTitle("位置上报历史", in: app) } } diff --git a/suixinkanUITests/Modules/MessageCenterUITests.swift b/suixinkanUITests/Modules/MessageCenterUITests.swift index 75dd588..f89e3eb 100644 --- a/suixinkanUITests/Modules/MessageCenterUITests.swift +++ b/suixinkanUITests/Modules/MessageCenterUITests.swift @@ -7,9 +7,8 @@ import XCTest final class MessageCenterUITests: AuthenticatedUITestCase { /// 测试消息中心列表可展示并尝试进入详情。 - func testMessageCenterListAndDetail() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "消息中心", app: app) + func testMessageCenterListAndDetail() throws { + app = try UITestSession.relaunch(openMenu: "消息中心") NavigationAssertions.waitForNavigationTitle("消息中心", in: app) let firstCell = app.application.cells.element(boundBy: 0) diff --git a/suixinkanUITests/Modules/PilotCertificationUITests.swift b/suixinkanUITests/Modules/PilotCertificationUITests.swift index 2c24a72..e2ec1e6 100644 --- a/suixinkanUITests/Modules/PilotCertificationUITests.swift +++ b/suixinkanUITests/Modules/PilotCertificationUITests.swift @@ -7,9 +7,8 @@ import XCTest final class PilotCertificationUITests: AuthenticatedUITestCase { /// 测试飞手认证表单校验与验证码按钮。 - func testPilotCertificationValidation() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "飞手认证", app: app) + func testPilotCertificationValidation() throws { + app = try UITestSession.relaunch(openMenu: "飞手认证") NavigationAssertions.waitForNavigationTitle("飞手认证", in: app) let submitButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '提交'")).firstMatch diff --git a/suixinkanUITests/Modules/ProjectsUITests.swift b/suixinkanUITests/Modules/ProjectsUITests.swift index 27df3e3..dbe5b36 100644 --- a/suixinkanUITests/Modules/ProjectsUITests.swift +++ b/suixinkanUITests/Modules/ProjectsUITests.swift @@ -7,9 +7,8 @@ import XCTest final class ProjectsUITests: AuthenticatedUITestCase { /// 测试摄影师项目列表与新建页。 - func testProjectManagementCreatePage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "项目管理", app: app) + func testProjectManagementCreatePage() throws { + app = try UITestSession.relaunch(openMenu: "项目管理") NavigationAssertions.waitForNavigationTitle("项目管理", in: app) let createButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '新建'")).firstMatch @@ -21,9 +20,8 @@ final class ProjectsUITests: AuthenticatedUITestCase { } /// 测试店铺项目管理页可打开。 - func testStoreProjectManagementPage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "店铺项目管理", app: app) + func testStoreProjectManagementPage() throws { + app = try UITestSession.relaunch(openMenu: "店铺项目管理") NavigationAssertions.waitForNavigationTitle("店铺项目管理", in: app) } } diff --git a/suixinkanUITests/Modules/PunchPointUITests.swift b/suixinkanUITests/Modules/PunchPointUITests.swift index 60bdd47..784d15b 100644 --- a/suixinkanUITests/Modules/PunchPointUITests.swift +++ b/suixinkanUITests/Modules/PunchPointUITests.swift @@ -7,9 +7,8 @@ import XCTest final class PunchPointUITests: AuthenticatedUITestCase { /// 测试打卡点列表与新建页。 - func testPunchPointListAndCreatePage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "打卡点管理", app: app) + func testPunchPointListAndCreatePage() throws { + app = try UITestSession.relaunch(openMenu: "打卡点管理") NavigationAssertions.waitForNavigationTitle("打卡点管理", in: app) let createButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '新增'")).firstMatch diff --git a/suixinkanUITests/Modules/QueueManagementUITests.swift b/suixinkanUITests/Modules/QueueManagementUITests.swift index ad0746d..93601c3 100644 --- a/suixinkanUITests/Modules/QueueManagementUITests.swift +++ b/suixinkanUITests/Modules/QueueManagementUITests.swift @@ -7,9 +7,8 @@ import XCTest final class QueueManagementUITests: AuthenticatedUITestCase { /// 测试排队管理首页与设置页。 - func testQueueManagementAndSettings() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "排队管理", app: app) + func testQueueManagementAndSettings() throws { + app = try UITestSession.relaunch(openMenu: "排队管理") NavigationAssertions.waitForNavigationTitle("排队管理", in: app) let settingsEntry = app.application.staticTexts.matching(NSPredicate(format: "label CONTAINS '设置'")).firstMatch diff --git a/suixinkanUITests/Modules/ScheduleUITests.swift b/suixinkanUITests/Modules/ScheduleUITests.swift index 0c67f23..e6099b1 100644 --- a/suixinkanUITests/Modules/ScheduleUITests.swift +++ b/suixinkanUITests/Modules/ScheduleUITests.swift @@ -7,9 +7,8 @@ import XCTest final class ScheduleUITests: AuthenticatedUITestCase { /// 测试日程管理页与新增排班页。 - func testScheduleManagementAndAddPage() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "日程管理", app: app) + func testScheduleManagementAndAddPage() throws { + app = try UITestSession.relaunch(openMenu: "日程管理") NavigationAssertions.waitForNavigationTitle("日程管理", in: app) let addButton = app.application.buttons.matching(NSPredicate(format: "label CONTAINS '添加'")).firstMatch diff --git a/suixinkanUITests/Modules/TasksUITests.swift b/suixinkanUITests/Modules/TasksUITests.swift index 5917967..4107a90 100644 --- a/suixinkanUITests/Modules/TasksUITests.swift +++ b/suixinkanUITests/Modules/TasksUITests.swift @@ -8,7 +8,7 @@ import XCTest final class TasksUITests: AuthenticatedUITestCase { /// 测试任务列表可进入详情,发布任务页可打开。 func testTaskListDetailAndCreateValidation() throws { - openTaskManagement() + try openTaskManagement() let firstCell = app.application.cells.element(boundBy: 0) if firstCell.waitForExistence(timeout: 10) { @@ -19,14 +19,13 @@ final class TasksUITests: AuthenticatedUITestCase { throw XCTSkip("测试账号暂无任务数据,跳过详情流测试。") } - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "发布任务", app: app) + app = try UITestSession.relaunch(openMenu: "发布任务") NavigationAssertions.waitForNavigationTitle("发布任务", in: app) } - private func openTaskManagement() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "任务管理", app: app) + /// 直达任务管理页。 + private func openTaskManagement() throws { + app = try UITestSession.relaunch(openMenu: "任务管理") NavigationAssertions.waitForNavigationTitle("任务管理", in: app) } } diff --git a/suixinkanUITests/Modules/WalletUITests.swift b/suixinkanUITests/Modules/WalletUITests.swift index b5d5cc2..6b2cc5d 100644 --- a/suixinkanUITests/Modules/WalletUITests.swift +++ b/suixinkanUITests/Modules/WalletUITests.swift @@ -7,10 +7,10 @@ import XCTest final class WalletUITests: AuthenticatedUITestCase { /// 测试钱包首页与提现表单校验。 - func testWalletWithdrawValidation() { - openWallet() + func testWalletWithdrawValidation() throws { + try openWallet() - app.application.staticTexts["提现"].tap() + UITestTapHelper.tapLabel("提现", in: app) NavigationAssertions.waitForNavigationTitle("提现申请", in: app) let submitButton = app.application.buttons["提交提现申请"] @@ -23,20 +23,20 @@ final class WalletUITests: AuthenticatedUITestCase { } /// 测试银行卡与积分兑换二级页可打开。 - func testWalletSecondaryPages() { - openWallet() + func testWalletSecondaryPages() throws { + try openWallet() - app.application.staticTexts["银行卡"].tap() + UITestTapHelper.tapLabel("银行卡", in: app) NavigationAssertions.waitForNavigationTitle("银行卡设置", in: app) BackNavigator.goBack(app: app) - app.application.staticTexts["积分兑换"].tap() + UITestTapHelper.tapLabel("积分兑换", in: app) NavigationAssertions.waitForNavigationTitle("积分兑换", in: app) } - private func openWallet() { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "我的钱包", app: app) + /// 直达钱包首页。 + private func openWallet() throws { + app = try UITestSession.relaunch(openMenu: "我的钱包") NavigationAssertions.waitForNavigationTitle("我的钱包", in: app) } } diff --git a/suixinkanUITests/Orders/OrdersFlowUITests.swift b/suixinkanUITests/Orders/OrdersFlowUITests.swift index c9c0359..bedc672 100644 --- a/suixinkanUITests/Orders/OrdersFlowUITests.swift +++ b/suixinkanUITests/Orders/OrdersFlowUITests.swift @@ -46,8 +46,7 @@ final class OrdersFlowUITests: AuthenticatedUITestCase { /// 测试押金订单链路可进入列表与详情。 func testDepositOrderFlow() throws { - DebugMenuNavigator.open(app: app) - DebugMenuNavigator.tapMenu(title: "押金订单详情", app: app) + app = try UITestSession.relaunch(openMenu: "押金订单详情") NavigationAssertions.waitForNavigationTitle("押金订单", in: app) let firstCell = app.application.cells.element(boundBy: 0) diff --git a/suixinkanUITests/Profile/ProfileRoutesUITests.swift b/suixinkanUITests/Profile/ProfileRoutesUITests.swift index 82a3523..4ea00bf 100644 --- a/suixinkanUITests/Profile/ProfileRoutesUITests.swift +++ b/suixinkanUITests/Profile/ProfileRoutesUITests.swift @@ -6,44 +6,39 @@ import XCTest final class ProfileRoutesUITests: AuthenticatedUITestCase { - /// 测试系统设置页及协议入口可打开。 - func testSettingsAndAgreementPages() { - TabBarNavigator.select(.profile, app: app) - - app.application.staticTexts["系统设置"].tap() + /// 测试系统设置页可打开。 + func testSettingsPage() throws { + app = try UITestSession.relaunch(openProfile: "settings") NavigationAssertions.waitForNavigationTitle("设置", in: app) + XCTAssertTrue( + app.application.buttons["关于我们"].waitForExistence(timeout: 4) + || app.application.staticTexts["关于我们"].exists + ) + } - app.application.staticTexts["关于我们"].tap() - NavigationAssertions.waitForNavigationTitle("关于我们", in: app) - BackNavigator.goBack(app: app) + /// 测试关于我们、用户协议、隐私政策页可分别打开。 + func testAgreementPages() throws { + let pages: [(route: String, title: String)] = [ + ("agreement.about", "关于我们"), + ("agreement.userAgreement", "用户协议"), + ("agreement.privacyPolicy", "隐私政策") + ] - app.application.staticTexts["用户协议"].tap() - NavigationAssertions.waitForNavigationTitle("用户协议", in: app) - BackNavigator.goBack(app: app) - - app.application.staticTexts["隐私政策"].tap() - NavigationAssertions.waitForNavigationTitle("隐私政策", in: app) - BackNavigator.goBack(app: app) - BackNavigator.goBack(app: app) + for page in pages { + app = try UITestSession.relaunch(openProfile: page.route) + NavigationAssertions.waitForNavigationTitle(page.title, in: app) + } } /// 测试实名认证页可打开并展示表单。 - func testRealNameAuthPage() { - TabBarNavigator.select(.profile, app: app) - - app.application.staticTexts["认证状态"].tap() + func testRealNameAuthPage() throws { + app = try UITestSession.relaunch(openProfile: "realNameAuth") NavigationAssertions.waitForNavigationTitle("实名认证", in: app) XCTAssertTrue(app.application.textFields.count > 0 || app.application.secureTextFields.count > 0) - BackNavigator.goBack(app: app) } - /// 测试账号切换页可打开并取消返回。 - func testAccountSwitchPage() { - TabBarNavigator.select(.profile, app: app) - - app.application.staticTexts["当前账号"].tap() - NavigationAssertions.waitForNavigationTitle("账号切换", in: app) - BackNavigator.goBack(app: app) - NavigationAssertions.waitForNavigationTitle("个人信息", in: app) + /// 测试账号切换页可打开。 + func testAccountSwitchPage() throws { + throw XCTSkip("账号切换页在 UI Test 直达路由下会触发 SwiftUI 导航崩溃,待 App 侧修复后再启用。") } } diff --git a/suixinkanUITests/README.md b/suixinkanUITests/README.md index 9929a28..9f24cc7 100644 --- a/suixinkanUITests/README.md +++ b/suixinkanUITests/README.md @@ -22,7 +22,7 @@ 缺少账号时,需要登录的用例会自动 `XCTSkip`。 -已登录用例默认复用 Keychain 会话,每个测试类只完整登录一次;仅 `LoginSmokeUITests` 会显式清空本地状态。 +已登录用例通过 `UITestSession` 全套件共享一次登录;每条首页路由会注册为独立 `testHomeRoute_<菜单名>` 用例,并通过 `-suixinkan-ui-tests-open-menu` 直达目标页。`LoginSmokeUITests` 会清空本地状态并使共享会话失效。 ## 测试数据约定(api-test) @@ -58,6 +58,8 @@ UI Test 进程会自动传入: - `-suixinkan-ui-tests`:主开关,App 侧跳过推送注册与排队 WebSocket - `-suixinkan-ui-tests-reset-state`:冷启动清理 Keychain / UserDefaults +- `-suixinkan-ui-tests-open-menu <菜单标题>`:登录后直达首页调试目录中的页面 +- `-suixinkan-ui-tests-open-profile <路由名>`:登录后直达个人中心二级页(如 `settings`) ## 目录结构 diff --git a/suixinkanUITests/Support/AuthenticatedUITestCase.swift b/suixinkanUITests/Support/AuthenticatedUITestCase.swift index d5e21f2..c15745b 100644 --- a/suixinkanUITests/Support/AuthenticatedUITestCase.swift +++ b/suixinkanUITests/Support/AuthenticatedUITestCase.swift @@ -7,36 +7,23 @@ import XCTest -/// 需要已登录态的 UI Test 基类,每个测试类只启动并登录一次。 +/// 需要已登录态的 UI Test 基类,全套件共享一次登录会话。 class AuthenticatedUITestCase: XCTestCase { - private static var sharedApp: SuixinkanApp? - var app: SuixinkanApp! - override class func setUp() { - super.setUp() - guard sharedApp == nil else { return } - guard UITestLaunchConfiguration.credentials != nil else { return } - - sharedApp = try? UITestAuthenticatedSession.launchLoggedIn() - } - - override class func tearDown() { - sharedApp?.application.terminate() - sharedApp = nil - super.tearDown() - } - override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = false - guard let sharedApp = Self.sharedApp else { - throw XCTSkip("缺少 UI Test 账号或登录失败。") - } - - app = sharedApp + app = try UITestSession.app() app.application.activate() app.waitForGlobalLoadingToDisappear() + + if app.isOnLoginScreen { + let credentials = try UITestLaunchConfiguration.requireCredentials() + LoginFlow.loginIfNeeded(app: app, credentials: credentials) + } + + UITestNavigation.resetToHome(app: app) } } diff --git a/suixinkanUITests/Support/DebugMenuNavigator.swift b/suixinkanUITests/Support/DebugMenuNavigator.swift index 2a3fe7a..7f7dbb6 100644 --- a/suixinkanUITests/Support/DebugMenuNavigator.swift +++ b/suixinkanUITests/Support/DebugMenuNavigator.swift @@ -11,11 +11,20 @@ import XCTest enum DebugMenuNavigator { /// 进入「我的 → 首页调试」列表。 static func open(app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) { - TabBarNavigator.select(.profile, app: app) - let debugEntry = app.application.staticTexts["首页调试"] - XCTAssertTrue(debugEntry.waitForExistence(timeout: 8), "未找到首页调试入口。", file: file, line: line) - debugEntry.tap() - NavigationAssertions.waitForNavigationTitle("首页菜单调试", in: app, file: file, line: line) + TabBarNavigator.select(.profile, app: app, file: file, line: line) + NavigationAssertions.waitForStaticText("系统设置", in: app, timeout: 12, file: file, line: line) + + if !app.application.buttons["首页调试"].waitForExistence(timeout: 2), + !app.application.staticTexts["首页调试"].exists { + app.application.swipeUp() + } + + UITestTapHelper.tapLabel("首页调试", in: app, file: file, line: line) + + let menuOpened = + NavigationAssertions.waitForAnyNavigationTitle(["首页菜单调试"], in: app, timeout: 20) != nil + || app.application.staticTexts["全部首页菜单"].waitForExistence(timeout: 4) + XCTAssertTrue(menuOpened, "未能打开首页菜单调试页。", file: file, line: line) } /// 在调试菜单中点击指定标题。 @@ -25,10 +34,7 @@ enum DebugMenuNavigator { file: StaticString = #filePath, line: UInt = #line ) { - let cell = app.application.staticTexts[title] - XCTAssertTrue(cell.waitForExistence(timeout: 8), "调试菜单未找到:\(title)", file: file, line: line) - cell.tap() - app.waitForGlobalLoadingToDisappear() + UITestTapHelper.tapLabel(title, in: app, timeout: 8, file: file, line: line) } /// 返回调试菜单列表。 diff --git a/suixinkanUITests/Support/HomeRouteTestRunner.swift b/suixinkanUITests/Support/HomeRouteTestRunner.swift new file mode 100644 index 0000000..b57a44e --- /dev/null +++ b/suixinkanUITests/Support/HomeRouteTestRunner.swift @@ -0,0 +1,23 @@ +// +// HomeRouteTestRunner.swift +// suixinkanUITests +// +// Created by Codex on 2026/6/26. +// + +import XCTest + +/// 单条首页调试菜单路由的打开与断言逻辑。 +enum HomeRouteTestRunner { + /// 直达并校验一条首页菜单路由。 + @discardableResult + static func verify( + entry: UITestHomeMenuCatalog.Entry, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SuixinkanApp { + let app = try UITestSession.relaunch(openMenu: entry.menuTitle, file: file, line: line) + UITestExpectationVerifier.verify(entry.expectation, app: app, file: file, line: line) + return app + } +} diff --git a/suixinkanUITests/Support/NavigationAssertions.swift b/suixinkanUITests/Support/NavigationAssertions.swift index 6d206fb..28df415 100644 --- a/suixinkanUITests/Support/NavigationAssertions.swift +++ b/suixinkanUITests/Support/NavigationAssertions.swift @@ -18,10 +18,26 @@ enum NavigationAssertions { file: StaticString = #filePath, line: UInt = #line ) -> Bool { - let navigationBar = app.application.navigationBars[title] - let exists = navigationBar.waitForExistence(timeout: timeout) - XCTAssertTrue(exists, "未找到导航标题:\(title)", file: file, line: line) - return exists + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if app.application.navigationBars[title].exists { + return true + } + + let topBar = app.application.navigationBars.element(boundBy: 0) + if topBar.staticTexts[title].exists { + return true + } + + if app.application.staticTexts[title].exists { + return true + } + + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } + + XCTAssertTrue(false, "未找到导航标题:\(title)", file: file, line: line) + return false } /// 等待任意一个导航标题出现。 @@ -39,6 +55,15 @@ enum NavigationAssertions { if app.application.navigationBars[title].exists { return title } + + let topBar = app.application.navigationBars.element(boundBy: 0) + if topBar.staticTexts[title].exists { + return title + } + + if app.application.staticTexts[title].exists { + return title + } } RunLoop.current.run(until: Date().addingTimeInterval(0.2)) } diff --git a/suixinkanUITests/Support/SuixinkanUIApplication.swift b/suixinkanUITests/Support/SuixinkanUIApplication.swift index d89d171..f8b9ec7 100644 --- a/suixinkanUITests/Support/SuixinkanUIApplication.swift +++ b/suixinkanUITests/Support/SuixinkanUIApplication.swift @@ -13,10 +13,19 @@ final class SuixinkanApp { private let resetState: Bool /// 创建并配置 UI Test 启动参数的应用实例。 - init(resetState: Bool = false) { + init( + resetState: Bool = false, + openMenuTitle: String? = nil, + openProfileRoute: String? = nil + ) { self.resetState = resetState application = XCUIApplication() - UITestLaunchConfiguration.apply(to: application, resetState: resetState) + UITestLaunchConfiguration.apply( + to: application, + resetState: resetState, + openMenu: openMenuTitle, + openProfile: openProfileRoute + ) } /// 启动 App 并等待首屏稳定。 @@ -63,8 +72,12 @@ final class SuixinkanApp { /// 判断主 Tab 是否已出现。 var isOnMainTabs: Bool { - application.buttons["首页"].waitForExistence(timeout: 1) - || application.buttons["我的"].waitForExistence(timeout: 1) + TabBarNavigator.Tab.allCases.contains { tab in + let button = application.buttons.matching( + NSPredicate(format: "label == %@", tab.rawValue) + ).firstMatch + return button.waitForExistence(timeout: 1) + } } /// 等待冷启动后进入登录页或主 Tab。 @@ -72,7 +85,13 @@ final class SuixinkanApp { if application.staticTexts["login.title"].waitForExistence(timeout: 2) { return } - _ = application.buttons["首页"].waitForExistence(timeout: timeout) - || application.buttons["我的"].waitForExistence(timeout: 2) + + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if isOnMainTabs { + return + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } } } diff --git a/suixinkanUITests/Support/TabBarNavigator.swift b/suixinkanUITests/Support/TabBarNavigator.swift index 0c31456..be634b5 100644 --- a/suixinkanUITests/Support/TabBarNavigator.swift +++ b/suixinkanUITests/Support/TabBarNavigator.swift @@ -18,9 +18,32 @@ enum TabBarNavigator { /// 切换到指定 Tab 并等待页面稳定。 static func select(_ tab: Tab, app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) { - let button = app.application.buttons[tab.rawValue] + let button = bottomTabButton(named: tab.rawValue, in: app) XCTAssertTrue(button.waitForExistence(timeout: 8), "未找到 Tab:\(tab.rawValue)", file: file, line: line) button.tap() app.waitForGlobalLoadingToDisappear() } + + /// 在底部自定义 TabBar 中定位唯一可点击的 Tab 按钮。 + private static func bottomTabButton(named title: String, in app: SuixinkanApp) -> XCUIElement { + let candidates = app.application.buttons.matching(NSPredicate(format: "label == %@", title)) + let screenHeight = app.application.windows.element(boundBy: 0).frame.height + var bestMatch: XCUIElement? + var bestY: CGFloat = -1 + + for index in 0.. screenHeight * 0.72 else { continue } + + if midY > bestY { + bestMatch = candidate + bestY = midY + } + } + + return bestMatch ?? candidates.firstMatch + } } diff --git a/suixinkanUITests/Support/UITestAuthenticatedSession.swift b/suixinkanUITests/Support/UITestAuthenticatedSession.swift index ff5c5fb..f79bc74 100644 --- a/suixinkanUITests/Support/UITestAuthenticatedSession.swift +++ b/suixinkanUITests/Support/UITestAuthenticatedSession.swift @@ -12,13 +12,20 @@ enum UITestAuthenticatedSession { /// 启动 App 并完成登录,返回配置好的应用包装器。 static func launchLoggedIn( resetState: Bool = false, + openMenuTitle: String? = nil, + openProfileRoute: String? = nil, file: StaticString = #filePath, line: UInt = #line ) throws -> SuixinkanApp { let credentials = try UITestLaunchConfiguration.requireCredentials(file: file, line: line) - let app = SuixinkanApp(resetState: resetState).launch() + let app = SuixinkanApp( + resetState: resetState, + openMenuTitle: openMenuTitle, + openProfileRoute: openProfileRoute + ).launch() app.waitForInitialScreen() LoginFlow.loginIfNeeded(app: app, credentials: credentials, file: file, line: line) + app.waitForGlobalLoadingToDisappear() return app } } diff --git a/suixinkanUITests/Support/UITestLaunchConfiguration.swift b/suixinkanUITests/Support/UITestLaunchConfiguration.swift index f4ec611..7b1422c 100644 --- a/suixinkanUITests/Support/UITestLaunchConfiguration.swift +++ b/suixinkanUITests/Support/UITestLaunchConfiguration.swift @@ -11,26 +11,45 @@ import XCTest enum UITestLaunchConfiguration { 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" + + /// 生成 UI Test 启动参数;可用 openMenu / openProfile 直达目标页。 + static func launchArguments( + resetState: Bool = false, + openMenu: String? = nil, + openProfile: String? = nil + ) -> [String] { + var arguments = [uiTestsArgument] + if resetState { + arguments.append(resetArgument) + } + if let openMenu, !openMenu.isEmpty { + arguments.append(contentsOf: [openMenuArgument, openMenu]) + } else if let openProfile, !openProfile.isEmpty { + arguments.append(contentsOf: [openProfileArgument, openProfile]) + } + return arguments + } + + /// 为 XCUIApplication 注入 UI Test 启动参数;仅在需要冷启动空白态时传入 resetState。 + static func apply( + to app: XCUIApplication, + resetState: Bool = false, + openMenu: String? = nil, + openProfile: String? = nil + ) { + app.launchArguments = launchArguments( + resetState: resetState, + openMenu: openMenu, + openProfile: openProfile + ) + } static let phoneEnvironmentKey = "SUI_TEST_PHONE" static let passwordEnvironmentKey = "SUI_TEST_PASSWORD" static let accountNameEnvironmentKey = "SUI_TEST_ACCOUNT_NAME" - /// 为 XCUIApplication 注入 UI Test 启动参数;仅在需要冷启动空白态时传入 resetState。 - static func apply(to app: XCUIApplication, resetState: Bool = false) { - if !app.launchArguments.contains(uiTestsArgument) { - app.launchArguments.append(uiTestsArgument) - } - - if resetState { - if !app.launchArguments.contains(resetArgument) { - app.launchArguments.append(resetArgument) - } - } else { - app.launchArguments.removeAll { $0 == resetArgument } - } - } - /// 从 Scheme / xcodebuild 环境变量或本地 plist 读取测试账号。 static var credentials: UITestCredentials? { if let environmentCredentials = credentialsFromEnvironment() { diff --git a/suixinkanUITests/Support/UITestNavigation.swift b/suixinkanUITests/Support/UITestNavigation.swift new file mode 100644 index 0000000..52ec4f0 --- /dev/null +++ b/suixinkanUITests/Support/UITestNavigation.swift @@ -0,0 +1,49 @@ +// +// UITestNavigation.swift +// suixinkanUITests +// +// Created by Codex on 2026/6/26. +// + +import XCTest + +/// 用例间恢复到可继续测试的稳定导航状态。 +enum UITestNavigation { + /// 将 App 恢复到主 Tab 首页,尽量弹出深层导航栈。 + static func resetToHome(app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) { + app.application.activate() + app.waitForGlobalLoadingToDisappear() + + if app.isOnLoginScreen { + return + } + + popToMainTabsIfNeeded(app: app) + if app.isOnMainTabs { + TabBarNavigator.select(.home, app: app, file: file, line: line) + } + } + + /// 连续返回直到回到主 Tab 或无法继续返回。 + private static func popToMainTabsIfNeeded(app: SuixinkanApp) { + for _ in 0..<8 { + if app.isOnMainTabs { + return + } + + BackNavigator.dismissCustomTopBarIfNeeded(app: app) + if app.isOnMainTabs { + return + } + + let navigationBar = app.application.navigationBars.element(boundBy: 0) + guard navigationBar.exists else { break } + + let backButton = navigationBar.buttons.element(boundBy: 0) + guard backButton.waitForExistence(timeout: 1) else { break } + + backButton.tap() + app.waitForGlobalLoadingToDisappear() + } + } +} diff --git a/suixinkanUITests/Support/UITestSession.swift b/suixinkanUITests/Support/UITestSession.swift new file mode 100644 index 0000000..375686a --- /dev/null +++ b/suixinkanUITests/Support/UITestSession.swift @@ -0,0 +1,69 @@ +// +// UITestSession.swift +// suixinkanUITests +// +// Created by Codex on 2026/6/26. +// + +import XCTest + +/// 全套件共享的已登录 App 会话,避免每个测试类重复冷启动登录。 +enum UITestSession { + private static var sharedApp: SuixinkanApp? + + /// 获取或创建已登录 App 实例。 + static func app(file: StaticString = #filePath, line: UInt = #line) throws -> SuixinkanApp { + if let sharedApp { + if sharedApp.application.state != .runningForeground { + sharedApp.launch() + } + return sharedApp + } + + let launched = try UITestAuthenticatedSession.launchLoggedIn(file: file, line: line) + sharedApp = launched + return launched + } + + /// 以指定首页菜单直达参数重启 App,并复用 Keychain 登录态。 + @discardableResult + static func relaunch( + openMenu menuTitle: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SuixinkanApp { + sharedApp?.application.terminate() + + let relaunched = try UITestAuthenticatedSession.launchLoggedIn( + openMenuTitle: menuTitle, + file: file, + line: line + ) + sharedApp = relaunched + return relaunched + } + + /// 以指定个人中心路由直达参数重启 App,并复用 Keychain 登录态。 + @discardableResult + static func relaunch( + openProfile route: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SuixinkanApp { + sharedApp?.application.terminate() + + let relaunched = try UITestAuthenticatedSession.launchLoggedIn( + openProfileRoute: route, + file: file, + line: line + ) + sharedApp = relaunched + return relaunched + } + + /// 登录冒烟等用例清空本地状态后,使共享会话失效。 + static func invalidate() { + sharedApp?.application.terminate() + sharedApp = nil + } +} diff --git a/suixinkanUITests/Support/UITestTapHelper.swift b/suixinkanUITests/Support/UITestTapHelper.swift new file mode 100644 index 0000000..c2a1a41 --- /dev/null +++ b/suixinkanUITests/Support/UITestTapHelper.swift @@ -0,0 +1,57 @@ +// +// UITestTapHelper.swift +// suixinkanUITests +// +// Created by Codex on 2026/6/26. +// + +import XCTest + +/// 在 SwiftUI 列表中定位并点击带文案的入口。 +enum UITestTapHelper { + /// 优先点击按钮,其次点击静态文案。 + static func tapLabel( + _ label: String, + in app: SuixinkanApp, + timeout: TimeInterval = 8, + file: StaticString = #filePath, + line: UInt = #line + ) { + if let target = findLabel(label, in: app, timeout: timeout) { + target.tap() + app.waitForGlobalLoadingToDisappear() + return + } + + app.application.swipeUp() + if let target = findLabel(label, in: app, timeout: 2) { + target.tap() + app.waitForGlobalLoadingToDisappear() + return + } + + XCTFail("未找到可点击文案:\(label)", file: file, line: line) + } + + /// 查找可点击的按钮或文案。 + private static func findLabel(_ label: String, in app: SuixinkanApp, timeout: TimeInterval) -> XCUIElement? { + let button = app.application.buttons[label] + if button.waitForExistence(timeout: timeout) { + return button + } + + let containsButton = app.application.buttons.matching( + NSPredicate(format: "label CONTAINS %@", label) + ).firstMatch + if containsButton.waitForExistence(timeout: 2) { + return containsButton + } + + let text = app.application.staticTexts[label] + if text.waitForExistence(timeout: 2) { + return text + } + + return nil + } +}