Refactor iOS UI tests to blueprint architecture
This commit is contained in:
@@ -7,15 +7,22 @@ class BaseUITestCase: XCTestCase {
|
||||
let defaultTimeout: TimeInterval = 15
|
||||
let longTimeout: TimeInterval = 30
|
||||
|
||||
var includeResetStateLaunchArgument: Bool { true }
|
||||
var additionalLaunchArguments: [String] { [] }
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
|
||||
app.launchArguments = [
|
||||
var launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations",
|
||||
"--reset-state"
|
||||
"--disable-animations"
|
||||
]
|
||||
if includeResetStateLaunchArgument {
|
||||
launchArguments.append("--reset-state")
|
||||
}
|
||||
launchArguments.append(contentsOf: additionalLaunchArguments)
|
||||
app.launchArguments = launchArguments
|
||||
|
||||
app.launch()
|
||||
app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
|
||||
183
iosApp/CaseraUITests/Framework/RebuildSupport.swift
Normal file
183
iosApp/CaseraUITests/Framework/RebuildSupport.swift
Normal file
@@ -0,0 +1,183 @@
|
||||
import XCTest
|
||||
|
||||
struct RebuildTestUser {
|
||||
let username: String
|
||||
let email: String
|
||||
let password: String
|
||||
}
|
||||
|
||||
enum RebuildTestUserFactory {
|
||||
static func unique(prefix: String = "uit") -> RebuildTestUser {
|
||||
let stamp = Int(Date().timeIntervalSince1970)
|
||||
return RebuildTestUser(
|
||||
username: "\(prefix)_user_\(stamp)",
|
||||
email: "\(prefix)_\(stamp)@example.com",
|
||||
password: "Pass1234"
|
||||
)
|
||||
}
|
||||
|
||||
static var seeded: RebuildTestUser {
|
||||
RebuildTestUser(username: "testuser", email: "test@example.com", password: "TestPass123!")
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var authCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] }
|
||||
private var onboardingCodeField: XCUIElement { app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField] }
|
||||
private var authVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Authentication.verifyButton] }
|
||||
private var onboardingVerifyButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton] }
|
||||
|
||||
var codeField: XCUIElement {
|
||||
if authCodeField.exists { return authCodeField }
|
||||
return onboardingCodeField
|
||||
}
|
||||
|
||||
var verifyButton: XCUIElement {
|
||||
if authVerifyButton.exists { return authVerifyButton }
|
||||
if onboardingVerifyButton.exists { return onboardingVerifyButton }
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||
}
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let loaded = authCodeField.waitForExistence(timeout: timeout)
|
||||
|| onboardingCodeField.waitForExistence(timeout: timeout)
|
||||
|| authVerifyButton.waitForExistence(timeout: timeout)
|
||||
|| onboardingVerifyButton.waitForExistence(timeout: timeout)
|
||||
XCTAssertTrue(loaded, "Expected verification screen to load", file: file, line: line)
|
||||
}
|
||||
|
||||
func enterCode(_ code: String) {
|
||||
codeField.waitForExistenceOrFail(timeout: 10)
|
||||
codeField.forceTap()
|
||||
codeField.typeText(code)
|
||||
}
|
||||
|
||||
func submitCode() {
|
||||
verifyButton.waitForExistenceOrFail(timeout: 10)
|
||||
verifyButton.forceTap()
|
||||
}
|
||||
|
||||
func tapLogoutIfAvailable() {
|
||||
let logout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
if logout.waitForExistence(timeout: 3) {
|
||||
logout.forceTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainTabScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var tabBar: XCUIElement { app.tabBars.firstMatch }
|
||||
var mainRoot: XCUIElement { app.otherElements[UITestID.Root.mainTabs] }
|
||||
|
||||
var residencesTab: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Navigation.residencesTab]
|
||||
if byID.exists { return byID }
|
||||
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
}
|
||||
|
||||
var profileTab: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Navigation.profileTab]
|
||||
if byID.exists { return byID }
|
||||
return app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||
}
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
let loaded = mainRoot.waitForExistence(timeout: timeout)
|
||||
|| tabBar.waitForExistence(timeout: timeout)
|
||||
XCTAssertTrue(loaded, "Expected main app root to appear")
|
||||
}
|
||||
|
||||
func goToResidences() {
|
||||
residencesTab.waitForExistenceOrFail(timeout: 10)
|
||||
residencesTab.forceTap()
|
||||
}
|
||||
|
||||
func goToProfile() {
|
||||
profileTab.waitForExistenceOrFail(timeout: 10)
|
||||
profileTab.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResidenceListScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var addButton: XCUIElement {
|
||||
let byID = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if byID.exists { return byID }
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
|
||||
}
|
||||
|
||||
var list: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.residencesList] }
|
||||
var emptyState: XCUIElement { app.otherElements[AccessibilityIdentifiers.Residence.emptyStateView] }
|
||||
var residenceCard: XCUIElement { app.otherElements.matching(identifier: AccessibilityIdentifiers.Residence.residenceCard).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
var loaded = false
|
||||
repeat {
|
||||
loaded = list.exists
|
||||
|| emptyState.exists
|
||||
|| residenceCard.exists
|
||||
|| addButton.exists
|
||||
|| app.staticTexts["Residences"].exists
|
||||
if loaded { break }
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||
} while Date() < deadline
|
||||
|
||||
XCTAssertTrue(loaded, "Expected residences list screen to load")
|
||||
}
|
||||
|
||||
func openCreateResidence() {
|
||||
addButton.waitForExistenceOrFail(timeout: 10)
|
||||
addButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResidenceFormScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
var nameField: XCUIElement { app.textFields[AccessibilityIdentifiers.Residence.nameField] }
|
||||
var saveButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.saveButton] }
|
||||
var cancelButton: XCUIElement { app.buttons[AccessibilityIdentifiers.Residence.formCancelButton] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
XCTAssertTrue(nameField.waitForExistence(timeout: timeout), "Expected residence form")
|
||||
}
|
||||
|
||||
func enterName(_ value: String) {
|
||||
nameField.waitForExistenceOrFail(timeout: 10)
|
||||
nameField.forceTap()
|
||||
nameField.typeText(value)
|
||||
}
|
||||
|
||||
func save() { saveButton.waitForExistenceOrFail(timeout: 10); saveButton.forceTap() }
|
||||
func cancel() { cancelButton.waitForExistenceOrFail(timeout: 10); cancelButton.forceTap() }
|
||||
}
|
||||
|
||||
enum RebuildSessionAssertions {
|
||||
static func assertOnLogin(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: timeout)
|
||||
XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists, "Expected login state", file: file, line: line)
|
||||
}
|
||||
|
||||
static func assertOnMainApp(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let main = MainTabScreen(app: app)
|
||||
main.waitForLoad(timeout: timeout)
|
||||
XCTAssertTrue(
|
||||
app.otherElements[UITestID.Root.mainTabs].exists || main.tabBar.exists,
|
||||
"Expected main app state",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
static func assertOnVerification(_ app: XCUIApplication, timeout: TimeInterval = 15, file: StaticString = #filePath, line: UInt = #line) {
|
||||
let verify = VerificationScreen(app: app)
|
||||
verify.waitForLoad(timeout: timeout, file: file, line: line)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ struct UITestID {
|
||||
static let ready = "ui.app.ready"
|
||||
static let onboarding = "ui.root.onboarding"
|
||||
static let login = "ui.root.login"
|
||||
static let mainTabs = "ui.root.mainTabs"
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
@@ -88,7 +89,12 @@ struct OnboardingWelcomeScreen {
|
||||
}
|
||||
|
||||
func tapAlreadyHaveAccount() {
|
||||
loginButton.waitUntilHittable(timeout: 10).tap()
|
||||
loginButton.waitForExistenceOrFail(timeout: 10)
|
||||
if loginButton.isHittable {
|
||||
loginButton.tap()
|
||||
} else {
|
||||
loginButton.forceTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,20 +230,44 @@ struct RegisterScreen {
|
||||
}
|
||||
|
||||
func fill(username: String, email: String, password: String) {
|
||||
func advanceToNextField() {
|
||||
let keys = ["Next", "Return", "return", "Done", "done"]
|
||||
for key in keys {
|
||||
let button = app.keyboards.buttons[key]
|
||||
if button.waitForExistence(timeout: 1) && button.isHittable {
|
||||
button.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usernameField.waitForExistenceOrFail(timeout: 10)
|
||||
usernameField.forceTap()
|
||||
usernameField.typeText(username)
|
||||
advanceToNextField()
|
||||
|
||||
emailField.waitForExistenceOrFail(timeout: 10)
|
||||
emailField.forceTap()
|
||||
if !emailField.hasKeyboardFocus {
|
||||
emailField.forceTap()
|
||||
if !emailField.hasKeyboardFocus {
|
||||
advanceToNextField()
|
||||
emailField.forceTap()
|
||||
}
|
||||
}
|
||||
emailField.typeText(email)
|
||||
advanceToNextField()
|
||||
|
||||
passwordField.waitForExistenceOrFail(timeout: 10)
|
||||
passwordField.forceTap()
|
||||
if !passwordField.hasKeyboardFocus {
|
||||
passwordField.forceTap()
|
||||
}
|
||||
passwordField.typeText(password)
|
||||
advanceToNextField()
|
||||
|
||||
confirmPasswordField.waitForExistenceOrFail(timeout: 10)
|
||||
confirmPasswordField.forceTap()
|
||||
if !confirmPasswordField.hasKeyboardFocus {
|
||||
confirmPasswordField.forceTap()
|
||||
}
|
||||
confirmPasswordField.typeText(password)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,14 @@ enum TestFlows {
|
||||
|
||||
@discardableResult
|
||||
static func openRegisterFromLogin(app: XCUIApplication) -> RegisterScreen {
|
||||
let login = navigateToLoginFromOnboarding(app: app)
|
||||
let login: LoginScreen
|
||||
let loginRoot = app.otherElements[UITestID.Root.login]
|
||||
if loginRoot.exists || app.textFields[UITestID.Auth.usernameField].exists {
|
||||
login = LoginScreen(app: app)
|
||||
login.waitForLoad()
|
||||
} else {
|
||||
login = navigateToLoginFromOnboarding(app: app)
|
||||
}
|
||||
login.tapSignUp()
|
||||
|
||||
let register = RegisterScreen(app: app)
|
||||
|
||||
Reference in New Issue
Block a user