新增完整 UI Test 回归套件,并完善启动隔离逻辑。

按模块拆分 XCUITest 用例与 Support 基础设施,UI Test 运行时跳过推送与排队 WebSocket,并将 Podfile 最低版本对齐 iOS 16。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 10:45:00 +08:00
parent 703078352c
commit 0c01ee26c3
39 changed files with 1533 additions and 231 deletions

View 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?
}