新增完整 UI Test 回归套件,并完善启动隔离逻辑。
按模块拆分 XCUITest 用例与 Support 基础设施,UI Test 运行时跳过推送与排队 WebSocket,并将 Podfile 最低版本对齐 iOS 16。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
42
suixinkanUITests/Support/AuthenticatedUITestCase.swift
Normal file
42
suixinkanUITests/Support/AuthenticatedUITestCase.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// AuthenticatedUITestCase.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 需要已登录态的 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.application.activate()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
}
|
||||
34
suixinkanUITests/Support/BackNavigator.swift
Normal file
34
suixinkanUITests/Support/BackNavigator.swift
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// BackNavigator.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 导航返回封装。
|
||||
enum BackNavigator {
|
||||
/// 通过导航栏返回按钮回到上一页。
|
||||
static func goBack(app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let navigationBar = app.application.navigationBars.element(boundBy: 0)
|
||||
guard navigationBar.exists else {
|
||||
XCTFail("当前页面没有可返回的导航栏。", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
let backButton = navigationBar.buttons.element(boundBy: 0)
|
||||
XCTAssertTrue(backButton.waitForExistence(timeout: 4), "未找到返回按钮。", file: file, line: line)
|
||||
backButton.tap()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
|
||||
/// 关闭自定义顶栏页面(如全部功能页)。
|
||||
static func dismissCustomTopBarIfNeeded(app: SuixinkanApp) {
|
||||
let backCandidates = app.application.buttons.matching(NSPredicate(format: "label CONTAINS 'chevron.left'"))
|
||||
if backCandidates.count > 0 {
|
||||
backCandidates.element(boundBy: 0).tap()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
suixinkanUITests/Support/DebugMenuNavigator.swift
Normal file
42
suixinkanUITests/Support/DebugMenuNavigator.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// DebugMenuNavigator.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// DEBUG 首页菜单调试页导航封装。
|
||||
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)
|
||||
}
|
||||
|
||||
/// 在调试菜单中点击指定标题。
|
||||
static func tapMenu(
|
||||
title: String,
|
||||
app: SuixinkanApp,
|
||||
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()
|
||||
}
|
||||
|
||||
/// 返回调试菜单列表。
|
||||
static func returnToMenu(app: SuixinkanApp) {
|
||||
if app.application.navigationBars["首页菜单调试"].exists {
|
||||
return
|
||||
}
|
||||
BackNavigator.goBack(app: app)
|
||||
NavigationAssertions.waitForNavigationTitle("首页菜单调试", in: app)
|
||||
}
|
||||
}
|
||||
104
suixinkanUITests/Support/LoginFlow.swift
Normal file
104
suixinkanUITests/Support/LoginFlow.swift
Normal file
@ -0,0 +1,104 @@
|
||||
//
|
||||
// LoginFlow.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 登录与多账号选择流程封装。
|
||||
enum LoginFlow {
|
||||
/// 如当前在登录页则执行完整登录,否则直接返回。
|
||||
static func loginIfNeeded(
|
||||
app: SuixinkanApp,
|
||||
credentials: UITestCredentials,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
guard app.isOnLoginScreen else { return }
|
||||
|
||||
fillLoginForm(app: app, credentials: credentials)
|
||||
submitLogin(app: app)
|
||||
handleAccountSelectionIfNeeded(app: app, credentials: credentials)
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
|
||||
XCTAssertTrue(
|
||||
app.isOnMainTabs,
|
||||
"登录后应进入主 Tab。",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
/// 填写登录表单并勾选协议。
|
||||
static func fillLoginForm(app: SuixinkanApp, credentials: UITestCredentials) {
|
||||
let usernameField = app.application.textFields["login.username"]
|
||||
XCTAssertTrue(usernameField.waitForExistence(timeout: 8))
|
||||
usernameField.tap()
|
||||
usernameField.clearAndType(credentials.phone)
|
||||
|
||||
let passwordField = preferredPasswordField(in: app.application)
|
||||
XCTAssertTrue(passwordField.waitForExistence(timeout: 4))
|
||||
passwordField.tap()
|
||||
passwordField.clearAndType(credentials.password)
|
||||
|
||||
let privacyToggle = app.application.buttons["login.privacy"]
|
||||
if privacyToggle.exists {
|
||||
privacyToggle.tap()
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击登录按钮。
|
||||
static func submitLogin(app: SuixinkanApp) {
|
||||
let submitButton = app.application.buttons["login.submit"]
|
||||
XCTAssertTrue(submitButton.waitForExistence(timeout: 4))
|
||||
submitButton.tap()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
|
||||
/// 处理 v9 多账号选择页。
|
||||
static func handleAccountSelectionIfNeeded(
|
||||
app: SuixinkanApp,
|
||||
credentials: UITestCredentials
|
||||
) {
|
||||
let accountSelectionTitle = app.application.navigationBars["选择账号"]
|
||||
guard accountSelectionTitle.waitForExistence(timeout: 6) else { return }
|
||||
|
||||
if let accountName = credentials.accountName {
|
||||
let accountCell = app.application.staticTexts[accountName]
|
||||
if accountCell.waitForExistence(timeout: 4) {
|
||||
accountCell.tap()
|
||||
}
|
||||
}
|
||||
|
||||
let confirmButton = app.application.buttons["进入系统"]
|
||||
XCTAssertTrue(confirmButton.waitForExistence(timeout: 4))
|
||||
confirmButton.tap()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
|
||||
/// 优先读取安全输入框,兼容调试构建下的普通文本框。
|
||||
private static func preferredPasswordField(in app: XCUIApplication) -> XCUIElement {
|
||||
let secureField = app.secureTextFields["login.password"]
|
||||
if secureField.exists {
|
||||
return secureField
|
||||
}
|
||||
return app.textFields["login.password"]
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElement {
|
||||
/// 清空并重新输入文本。
|
||||
func clearAndType(_ text: String) {
|
||||
guard let currentValue = value as? String else {
|
||||
typeText(text)
|
||||
return
|
||||
}
|
||||
|
||||
tap()
|
||||
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
|
||||
typeText(deleteString)
|
||||
typeText(text)
|
||||
}
|
||||
}
|
||||
74
suixinkanUITests/Support/NavigationAssertions.swift
Normal file
74
suixinkanUITests/Support/NavigationAssertions.swift
Normal file
@ -0,0 +1,74 @@
|
||||
//
|
||||
// NavigationAssertions.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 页面导航与可见性断言工具。
|
||||
enum NavigationAssertions {
|
||||
/// 等待导航栏标题出现。
|
||||
@discardableResult
|
||||
static func waitForNavigationTitle(
|
||||
_ title: String,
|
||||
in app: SuixinkanApp,
|
||||
timeout: TimeInterval = 12,
|
||||
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
|
||||
}
|
||||
|
||||
/// 等待任意一个导航标题出现。
|
||||
@discardableResult
|
||||
static func waitForAnyNavigationTitle(
|
||||
_ titles: [String],
|
||||
in app: SuixinkanApp,
|
||||
timeout: TimeInterval = 12,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> String? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
for title in titles {
|
||||
if app.application.navigationBars[title].exists {
|
||||
return title
|
||||
}
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||
}
|
||||
|
||||
XCTFail("未找到任一导航标题:\(titles.joined(separator: " / "))", file: file, line: line)
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 等待静态文案出现。
|
||||
@discardableResult
|
||||
static func waitForStaticText(
|
||||
_ text: String,
|
||||
in app: SuixinkanApp,
|
||||
timeout: TimeInterval = 8,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> Bool {
|
||||
let label = app.application.staticTexts[text]
|
||||
let exists = label.waitForExistence(timeout: timeout)
|
||||
XCTAssertTrue(exists, "未找到文案:\(text)", file: file, line: line)
|
||||
return exists
|
||||
}
|
||||
|
||||
/// 断言页面未停留在全局 Loading。
|
||||
static func assertNotStuckOnLoading(
|
||||
app: SuixinkanApp,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
let loading = app.application.staticTexts["加载中"]
|
||||
XCTAssertFalse(loading.exists, "页面不应长期停留在全局 Loading。", file: file, line: line)
|
||||
}
|
||||
}
|
||||
78
suixinkanUITests/Support/SuixinkanUIApplication.swift
Normal file
78
suixinkanUITests/Support/SuixinkanUIApplication.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// SuixinkanUIApplication.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 封装 XCUIApplication 的常用等待、权限弹窗和全局 Loading 处理。
|
||||
final class SuixinkanApp {
|
||||
let application: XCUIApplication
|
||||
private let resetState: Bool
|
||||
|
||||
/// 创建并配置 UI Test 启动参数的应用实例。
|
||||
init(resetState: Bool = false) {
|
||||
self.resetState = resetState
|
||||
application = XCUIApplication()
|
||||
UITestLaunchConfiguration.apply(to: application, resetState: resetState)
|
||||
}
|
||||
|
||||
/// 启动 App 并等待首屏稳定。
|
||||
@discardableResult
|
||||
func launch(waitForLoading: Bool = true) -> Self {
|
||||
if application.state == .runningForeground {
|
||||
application.activate()
|
||||
} else {
|
||||
application.launch()
|
||||
}
|
||||
dismissSystemAlertsIfNeeded()
|
||||
if waitForLoading {
|
||||
waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// 等待全局 Loading 层消失。
|
||||
func waitForGlobalLoadingToDisappear(timeout: TimeInterval = 45) {
|
||||
let loading = application.staticTexts["加载中"]
|
||||
guard loading.waitForExistence(timeout: 2) else { return }
|
||||
|
||||
let predicate = NSPredicate(format: "exists == false")
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: loading)
|
||||
_ = XCTWaiter.wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
|
||||
/// 处理系统权限弹窗,尽量点允许以便页面继续加载。
|
||||
func dismissSystemAlertsIfNeeded() {
|
||||
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
let allowButtons = ["允许", "好", "使用App时允许", "Allow", "Allow While Using App", "OK"]
|
||||
for title in allowButtons {
|
||||
let button = springboard.buttons[title]
|
||||
if button.waitForExistence(timeout: 1) {
|
||||
button.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断当前是否仍在登录页。
|
||||
var isOnLoginScreen: Bool {
|
||||
application.staticTexts["login.title"].exists
|
||||
}
|
||||
|
||||
/// 判断主 Tab 是否已出现。
|
||||
var isOnMainTabs: Bool {
|
||||
application.buttons["首页"].waitForExistence(timeout: 1)
|
||||
|| application.buttons["我的"].waitForExistence(timeout: 1)
|
||||
}
|
||||
|
||||
/// 等待冷启动后进入登录页或主 Tab。
|
||||
func waitForInitialScreen(timeout: TimeInterval = 45) {
|
||||
if application.staticTexts["login.title"].waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
_ = application.buttons["首页"].waitForExistence(timeout: timeout)
|
||||
|| application.buttons["我的"].waitForExistence(timeout: 2)
|
||||
}
|
||||
}
|
||||
26
suixinkanUITests/Support/TabBarNavigator.swift
Normal file
26
suixinkanUITests/Support/TabBarNavigator.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// TabBarNavigator.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 主 Tab 导航封装。
|
||||
enum TabBarNavigator {
|
||||
enum Tab: String, CaseIterable {
|
||||
case home = "首页"
|
||||
case orders = "订单"
|
||||
case statistics = "数据"
|
||||
case profile = "我的"
|
||||
}
|
||||
|
||||
/// 切换到指定 Tab 并等待页面稳定。
|
||||
static func select(_ tab: Tab, app: SuixinkanApp, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let button = app.application.buttons[tab.rawValue]
|
||||
XCTAssertTrue(button.waitForExistence(timeout: 8), "未找到 Tab:\(tab.rawValue)", file: file, line: line)
|
||||
button.tap()
|
||||
app.waitForGlobalLoadingToDisappear()
|
||||
}
|
||||
}
|
||||
24
suixinkanUITests/Support/UITestAuthenticatedSession.swift
Normal file
24
suixinkanUITests/Support/UITestAuthenticatedSession.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// UITestAuthenticatedSession.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 已登录会话启动器,供需要主界面的 UI Test 复用。
|
||||
enum UITestAuthenticatedSession {
|
||||
/// 启动 App 并完成登录,返回配置好的应用包装器。
|
||||
static func launchLoggedIn(
|
||||
resetState: Bool = false,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) throws -> SuixinkanApp {
|
||||
let credentials = try UITestLaunchConfiguration.requireCredentials(file: file, line: line)
|
||||
let app = SuixinkanApp(resetState: resetState).launch()
|
||||
app.waitForInitialScreen()
|
||||
LoginFlow.loginIfNeeded(app: app, credentials: credentials, file: file, line: line)
|
||||
return app
|
||||
}
|
||||
}
|
||||
43
suixinkanUITests/Support/UITestExpectationVerifier.swift
Normal file
43
suixinkanUITests/Support/UITestExpectationVerifier.swift
Normal file
@ -0,0 +1,43 @@
|
||||
//
|
||||
// UITestExpectationVerifier.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// 根据首页菜单目录断言页面打开结果。
|
||||
enum UITestExpectationVerifier {
|
||||
/// 校验调试菜单打开后的目标页。
|
||||
static func verify(
|
||||
_ expectation: UITestHomeMenuCatalog.Expectation,
|
||||
app: SuixinkanApp,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
switch expectation {
|
||||
case .navigationTitle(let title):
|
||||
NavigationAssertions.waitForNavigationTitle(title, in: app, file: file, line: line)
|
||||
case .anyNavigationTitle(let titles):
|
||||
_ = NavigationAssertions.waitForAnyNavigationTitle(titles, in: app, file: file, line: line)
|
||||
case .tab(let tab):
|
||||
switch tab {
|
||||
case .orders:
|
||||
NavigationAssertions.waitForNavigationTitle("订单", in: app, file: file, line: line)
|
||||
case .statistics:
|
||||
NavigationAssertions.waitForNavigationTitle("数据", in: app, file: file, line: line)
|
||||
case .home:
|
||||
XCTAssertTrue(app.application.staticTexts["常用应用"].waitForExistence(timeout: 8) || app.application.staticTexts["请选择景区"].exists, file: file, line: line)
|
||||
case .profile:
|
||||
NavigationAssertions.waitForNavigationTitle("个人信息", in: app, file: file, line: line)
|
||||
}
|
||||
case .staticText(let text):
|
||||
NavigationAssertions.waitForStaticText(text, in: app, file: file, line: line)
|
||||
case .unsupportedPlaceholder(let title):
|
||||
_ = NavigationAssertions.waitForAnyNavigationTitle([title], in: app, file: file, line: line)
|
||||
}
|
||||
|
||||
NavigationAssertions.assertNotStuckOnLoading(app: app, file: file, line: line)
|
||||
}
|
||||
}
|
||||
73
suixinkanUITests/Support/UITestHomeMenuCatalog.swift
Normal file
73
suixinkanUITests/Support/UITestHomeMenuCatalog.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// UITestHomeMenuCatalog.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 首页调试菜单覆盖目录,按去重后的目标页组织。
|
||||
enum UITestHomeMenuCatalog {
|
||||
/// 打开菜单后的期望结果。
|
||||
enum Expectation: Equatable {
|
||||
case navigationTitle(String)
|
||||
case anyNavigationTitle([String])
|
||||
case tab(TabBarNavigator.Tab)
|
||||
case staticText(String)
|
||||
case unsupportedPlaceholder(title: String)
|
||||
}
|
||||
|
||||
/// 单个调试菜单用例。
|
||||
struct Entry: Equatable {
|
||||
let menuTitle: String
|
||||
let expectation: Expectation
|
||||
}
|
||||
|
||||
/// 去重后的首页路由覆盖表,同义 URI 只保留一个菜单标题。
|
||||
static let entries: [Entry] = [
|
||||
Entry(menuTitle: "空间设置", expectation: .navigationTitle("个人信息")),
|
||||
Entry(menuTitle: "相册管理", expectation: .navigationTitle("相册管理")),
|
||||
Entry(menuTitle: "相册预览上传", expectation: .navigationTitle("相册预览上传")),
|
||||
Entry(menuTitle: "我的钱包", expectation: .navigationTitle("我的钱包")),
|
||||
Entry(menuTitle: "立即收款", expectation: .navigationTitle("立即收款")),
|
||||
Entry(menuTitle: "相册云盘", expectation: .navigationTitle("相册云盘")),
|
||||
Entry(menuTitle: "传输管理", expectation: .navigationTitle("传输记录")),
|
||||
Entry(menuTitle: "素材管理", expectation: .navigationTitle("素材管理")),
|
||||
Entry(menuTitle: "上传素材", expectation: .navigationTitle("上传素材")),
|
||||
Entry(menuTitle: "任务管理", expectation: .navigationTitle("任务管理")),
|
||||
Entry(menuTitle: "发布任务", expectation: .navigationTitle("发布任务")),
|
||||
Entry(menuTitle: "日程管理", expectation: .navigationTitle("日程管理")),
|
||||
Entry(menuTitle: "消息中心", expectation: .navigationTitle("消息中心")),
|
||||
Entry(menuTitle: "打卡点管理", expectation: .navigationTitle("打卡点管理")),
|
||||
Entry(menuTitle: "样片管理", expectation: .navigationTitle("样片管理")),
|
||||
Entry(menuTitle: "上传样片", expectation: .navigationTitle("上传样片")),
|
||||
Entry(menuTitle: "直播管理", expectation: .navigationTitle("直播管理")),
|
||||
Entry(menuTitle: "直播相册", expectation: .navigationTitle("直播相册")),
|
||||
Entry(menuTitle: "景区选择", expectation: .navigationTitle("景区选择")),
|
||||
Entry(menuTitle: "景区申请", expectation: .navigationTitle("景区申请")),
|
||||
Entry(menuTitle: "权限申请", expectation: .navigationTitle("申请权限")),
|
||||
Entry(menuTitle: "权限申请状态", expectation: .anyNavigationTitle(["权限申请", "权限申请状态"])),
|
||||
Entry(menuTitle: "景区结算", expectation: .navigationTitle("景区结算")),
|
||||
Entry(menuTitle: "结算审核", expectation: .navigationTitle("结算审核")),
|
||||
Entry(menuTitle: "核销订单", expectation: .tab(.orders)),
|
||||
Entry(menuTitle: "景区订单", expectation: .tab(.orders)),
|
||||
Entry(menuTitle: "数据统计", expectation: .tab(.statistics)),
|
||||
Entry(menuTitle: "项目管理", expectation: .navigationTitle("项目管理")),
|
||||
Entry(menuTitle: "店铺项目管理", expectation: .navigationTitle("店铺项目管理")),
|
||||
Entry(menuTitle: "位置上报", expectation: .navigationTitle("位置上报")),
|
||||
Entry(menuTitle: "注册邀请", expectation: .navigationTitle("摄影师邀请")),
|
||||
Entry(menuTitle: "邀请记录", expectation: .navigationTitle("邀请记录")),
|
||||
Entry(menuTitle: "店铺管理", expectation: .staticText("全部功能")),
|
||||
Entry(menuTitle: "飞行管理", expectation: .unsupportedPlaceholder(title: "飞行管理")),
|
||||
Entry(menuTitle: "飞手认证", expectation: .navigationTitle("飞手认证")),
|
||||
Entry(menuTitle: "飞控", expectation: .unsupportedPlaceholder(title: "飞控")),
|
||||
Entry(menuTitle: "排队管理", expectation: .navigationTitle("排队管理")),
|
||||
Entry(menuTitle: "运营区域", expectation: .navigationTitle("运营区域")),
|
||||
Entry(menuTitle: "押金订单详情", expectation: .navigationTitle("押金订单")),
|
||||
Entry(menuTitle: "提现审核", expectation: .navigationTitle("提现审核")),
|
||||
Entry(menuTitle: "定位上报历史", expectation: .navigationTitle("位置上报历史")),
|
||||
Entry(menuTitle: "更多功能", expectation: .staticText("全部功能")),
|
||||
Entry(menuTitle: "系统设置", expectation: .navigationTitle("设置"))
|
||||
]
|
||||
}
|
||||
115
suixinkanUITests/Support/UITestLaunchConfiguration.swift
Normal file
115
suixinkanUITests/Support/UITestLaunchConfiguration.swift
Normal file
@ -0,0 +1,115 @@
|
||||
//
|
||||
// UITestLaunchConfiguration.swift
|
||||
// suixinkanUITests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
/// UI Test 启动参数与环境变量,需与 App 侧 `AppUITestLaunchState` 保持同名。
|
||||
enum UITestLaunchConfiguration {
|
||||
static let uiTestsArgument = "-suixinkan-ui-tests"
|
||||
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
||||
|
||||
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() {
|
||||
return environmentCredentials
|
||||
}
|
||||
return credentialsFromLocalPlist()
|
||||
}
|
||||
|
||||
/// 缺少测试账号时跳过需要真实登录的用例。
|
||||
static func requireCredentials(file: StaticString = #filePath, line: UInt = #line) throws -> UITestCredentials {
|
||||
guard let credentials else {
|
||||
throw XCTSkip(
|
||||
"缺少 UI Test 账号。请配置环境变量 \(phoneEnvironmentKey) / \(passwordEnvironmentKey),或在本机创建 UITestCredentials.local.plist。"
|
||||
)
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
/// 从 Scheme / xcodebuild 环境变量读取测试账号。
|
||||
private static func credentialsFromEnvironment() -> UITestCredentials? {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
guard
|
||||
let phone = environment[phoneEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!phone.isEmpty,
|
||||
let password = environment[passwordEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!password.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let accountName = environment[accountNameEnvironmentKey]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return UITestCredentials(
|
||||
phone: phone,
|
||||
password: password,
|
||||
accountName: accountName?.isEmpty == false ? accountName : nil
|
||||
)
|
||||
}
|
||||
|
||||
/// 从 gitignore 的本地 plist 读取测试账号,便于本机开发配置。
|
||||
private static func credentialsFromLocalPlist() -> UITestCredentials? {
|
||||
let plistURL = localCredentialsURL
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: plistURL.path),
|
||||
let dictionary = NSDictionary(contentsOf: plistURL) as? [String: String]
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard
|
||||
let phone = dictionary[phoneEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!phone.isEmpty,
|
||||
let password = dictionary[passwordEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!password.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let accountName = dictionary[accountNameEnvironmentKey]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return UITestCredentials(
|
||||
phone: phone,
|
||||
password: password,
|
||||
accountName: accountName?.isEmpty == false ? accountName : nil
|
||||
)
|
||||
}
|
||||
|
||||
/// 本地测试账号 plist 路径。
|
||||
private static var localCredentialsURL: URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("UITestCredentials.local.plist")
|
||||
}
|
||||
}
|
||||
|
||||
/// UI Test 登录账号实体。
|
||||
struct UITestCredentials: Equatable {
|
||||
let phone: String
|
||||
let password: String
|
||||
let accountName: String?
|
||||
}
|
||||
Reference in New Issue
Block a user