移除 LoginViewModel 硬编码账号,扩展登录流程测试覆盖;调整 ZLoginSmokeUITests 执行顺序与路由直达逻辑,避免清空 Keychain 影响其它用例。 Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
4.1 KiB
Swift
125 lines
4.1 KiB
Swift
//
|
||
// 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 }
|
||
|
||
app.application.activate()
|
||
app.waitForGlobalLoadingToDisappear()
|
||
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: 12))
|
||
|
||
let currentPhone = usernameField.value as? String
|
||
if currentPhone != credentials.phone {
|
||
tapWhenReady(usernameField)
|
||
usernameField.clearAndType(credentials.phone)
|
||
}
|
||
|
||
let passwordField = preferredPasswordField(in: app.application)
|
||
XCTAssertTrue(passwordField.waitForExistence(timeout: 4))
|
||
tapWhenReady(passwordField)
|
||
passwordField.clearAndType(credentials.password)
|
||
|
||
let privacyToggle = app.application.buttons["login.privacy"]
|
||
if privacyToggle.exists, privacyToggle.isHittable {
|
||
privacyToggle.tap()
|
||
}
|
||
}
|
||
|
||
/// 点击登录按钮。
|
||
static func submitLogin(app: SuixinkanApp) {
|
||
let submitButton = app.application.buttons["login.submit"]
|
||
XCTAssertTrue(submitButton.waitForExistence(timeout: 4))
|
||
tapWhenReady(submitButton)
|
||
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 static func tapWhenReady(_ element: XCUIElement) {
|
||
let deadline = Date().addingTimeInterval(8)
|
||
while Date() < deadline {
|
||
if element.isHittable {
|
||
element.tap()
|
||
return
|
||
}
|
||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||
}
|
||
|
||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||
}
|
||
}
|
||
|
||
private extension XCUIElement {
|
||
/// 清空并重新输入文本。
|
||
func clearAndType(_ text: String) {
|
||
guard let currentValue = value as? String, !currentValue.isEmpty else {
|
||
typeText(text)
|
||
return
|
||
}
|
||
|
||
tap()
|
||
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
|
||
typeText(deleteString)
|
||
typeText(text)
|
||
}
|
||
}
|