Refactor iOS UI tests to blueprint architecture and migrate legacy suites
This commit is contained in:
117
iosApp/CaseraUITests/Framework/BaseUITestCase.swift
Normal file
117
iosApp/CaseraUITests/Framework/BaseUITestCase.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
import XCTest
|
||||
|
||||
class BaseUITestCase: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
let shortTimeout: TimeInterval = 5
|
||||
let defaultTimeout: TimeInterval = 15
|
||||
let longTimeout: TimeInterval = 30
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
|
||||
app.launchArguments = [
|
||||
"--ui-testing",
|
||||
"--disable-animations",
|
||||
"--reset-state"
|
||||
]
|
||||
|
||||
app.launch()
|
||||
app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
if let run = testRun, !run.hasSucceeded {
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Failure-\(name)"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XCUIElement {
|
||||
@discardableResult
|
||||
func waitForExistenceOrFail(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> XCUIElement {
|
||||
if !waitForExistence(timeout: timeout) {
|
||||
XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func waitUntilHittable(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> XCUIElement {
|
||||
let predicate = NSPredicate(format: "exists == true AND hittable == true")
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
|
||||
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
|
||||
|
||||
if result != .completed {
|
||||
XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func waitForNonExistence(
|
||||
timeout: TimeInterval,
|
||||
message: String? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) -> Bool {
|
||||
let predicate = NSPredicate(format: "exists == false")
|
||||
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
|
||||
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
|
||||
|
||||
if result != .completed {
|
||||
XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func scrollIntoView(
|
||||
in scrollView: XCUIElement,
|
||||
maxSwipes: Int = 8,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
if isHittable { return }
|
||||
|
||||
for _ in 0..<maxSwipes {
|
||||
scrollView.swipeUp()
|
||||
if isHittable { return }
|
||||
}
|
||||
|
||||
for _ in 0..<maxSwipes {
|
||||
scrollView.swipeDown()
|
||||
if isHittable { return }
|
||||
}
|
||||
|
||||
XCTFail("Failed to scroll element into view: \(self)", file: file, line: line)
|
||||
}
|
||||
|
||||
func forceTap(file: StaticString = #filePath, line: UInt = #line) {
|
||||
if isHittable {
|
||||
tap()
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
return
|
||||
}
|
||||
XCTFail("Expected element to exist before forceTap: \(self)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
247
iosApp/CaseraUITests/Framework/ScreenObjects.swift
Normal file
247
iosApp/CaseraUITests/Framework/ScreenObjects.swift
Normal file
@@ -0,0 +1,247 @@
|
||||
import XCTest
|
||||
|
||||
struct UITestID {
|
||||
struct Root {
|
||||
static let ready = "ui.app.ready"
|
||||
static let onboarding = "ui.root.onboarding"
|
||||
static let login = "ui.root.login"
|
||||
}
|
||||
|
||||
struct Onboarding {
|
||||
static let welcomeTitle = "Onboarding.WelcomeTitle"
|
||||
static let startFreshButton = "Onboarding.StartFreshButton"
|
||||
static let joinExistingButton = "Onboarding.JoinExistingButton"
|
||||
static let loginButton = "Onboarding.LoginButton"
|
||||
static let valuePropsContainer = "Onboarding.ValuePropsTitle"
|
||||
static let valuePropsNextButton = "Onboarding.ValuePropsNextButton"
|
||||
static let nameResidenceTitle = "Onboarding.NameResidenceTitle"
|
||||
static let residenceNameField = "Onboarding.ResidenceNameField"
|
||||
static let nameResidenceContinueButton = "Onboarding.NameResidenceContinueButton"
|
||||
static let createAccountTitle = "Onboarding.CreateAccountTitle"
|
||||
static let emailSignUpExpandButton = "Onboarding.EmailSignUpExpandButton"
|
||||
static let createAccountButton = "Onboarding.CreateAccountButton"
|
||||
static let backButton = "Onboarding.BackButton"
|
||||
static let skipButton = "Onboarding.SkipButton"
|
||||
static let progressIndicator = "Onboarding.ProgressIndicator"
|
||||
}
|
||||
|
||||
struct Auth {
|
||||
static let usernameField = "Login.UsernameField"
|
||||
static let passwordField = "Login.PasswordField"
|
||||
static let passwordVisibilityToggle = "Login.PasswordVisibilityToggle"
|
||||
static let loginButton = "Login.LoginButton"
|
||||
static let signUpButton = "Login.SignUpButton"
|
||||
static let forgotPasswordButton = "Login.ForgotPasswordButton"
|
||||
|
||||
static let registerUsernameField = "Register.UsernameField"
|
||||
static let registerEmailField = "Register.EmailField"
|
||||
static let registerPasswordField = "Register.PasswordField"
|
||||
static let registerConfirmPasswordField = "Register.ConfirmPasswordField"
|
||||
static let registerButton = "Register.RegisterButton"
|
||||
static let registerCancelButton = "Register.CancelButton"
|
||||
}
|
||||
}
|
||||
|
||||
struct RootScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
func waitForReady(timeout: TimeInterval = 15) {
|
||||
app.otherElements[UITestID.Root.ready].waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingWelcomeScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var onboardingRoot: XCUIElement { app.otherElements[UITestID.Root.onboarding] }
|
||||
private var startFreshButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.startFreshButton).firstMatch }
|
||||
private var joinExistingButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.joinExistingButton).firstMatch }
|
||||
private var loginButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.loginButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
onboardingRoot.waitForExistenceOrFail(timeout: timeout)
|
||||
if startFreshButton.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
|
||||
for _ in 0..<4 {
|
||||
if backButton.exists && backButton.isHittable {
|
||||
backButton.tap()
|
||||
}
|
||||
if startFreshButton.waitForExistence(timeout: 2) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !startFreshButton.waitForExistence(timeout: timeout) {
|
||||
XCTFail("Expected onboarding welcome entry point. Debug tree:\n\(app.debugDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func tapStartFresh() {
|
||||
startFreshButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapJoinExisting() {
|
||||
joinExistingButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapAlreadyHaveAccount() {
|
||||
loginButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingValuePropsScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var container: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.valuePropsContainer).firstMatch }
|
||||
private var continueButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.valuePropsNextButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
container.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func tapContinue() {
|
||||
continueButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapBack() {
|
||||
backButton.waitForExistenceOrFail(timeout: 10)
|
||||
backButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingNameResidenceScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var title: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.nameResidenceTitle).firstMatch }
|
||||
private var nameField: XCUIElement { app.textFields[UITestID.Onboarding.residenceNameField] }
|
||||
private var continueButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.nameResidenceContinueButton).firstMatch }
|
||||
private var backButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.backButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
title.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func enterResidenceName(_ value: String) {
|
||||
nameField.waitUntilHittable(timeout: 10).tap()
|
||||
nameField.typeText(value)
|
||||
}
|
||||
|
||||
func tapContinue() {
|
||||
continueButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapBack() {
|
||||
backButton.waitForExistenceOrFail(timeout: 10)
|
||||
backButton.forceTap()
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingCreateAccountScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var title: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.createAccountTitle).firstMatch }
|
||||
private var expandEmailButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.emailSignUpExpandButton).firstMatch }
|
||||
private var createAccountButton: XCUIElement { app.descendants(matching: .any).matching(identifier: UITestID.Onboarding.createAccountButton).firstMatch }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
title.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func expandEmailSignup() {
|
||||
expandEmailButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func waitForCreateAccountButton(timeout: TimeInterval = 10) {
|
||||
createAccountButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var usernameField: XCUIElement { app.textFields[UITestID.Auth.usernameField] }
|
||||
private var passwordSecureField: XCUIElement { app.secureTextFields[UITestID.Auth.passwordField] }
|
||||
private var passwordVisibleField: XCUIElement { app.textFields[UITestID.Auth.passwordField] }
|
||||
private var loginButton: XCUIElement { app.buttons[UITestID.Auth.loginButton] }
|
||||
private var signUpButton: XCUIElement { app.buttons[UITestID.Auth.signUpButton] }
|
||||
private var forgotPasswordButton: XCUIElement { app.buttons[UITestID.Auth.forgotPasswordButton] }
|
||||
private var visibilityToggle: XCUIElement { app.buttons[UITestID.Auth.passwordVisibilityToggle] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
usernameField.waitForExistenceOrFail(timeout: timeout)
|
||||
loginButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func enterUsername(_ username: String) {
|
||||
usernameField.waitUntilHittable(timeout: 10).tap()
|
||||
usernameField.typeText(username)
|
||||
}
|
||||
|
||||
func enterPassword(_ password: String) {
|
||||
if passwordSecureField.exists {
|
||||
passwordSecureField.tap()
|
||||
passwordSecureField.typeText(password)
|
||||
} else {
|
||||
passwordVisibleField.waitUntilHittable(timeout: 10).tap()
|
||||
passwordVisibleField.typeText(password)
|
||||
}
|
||||
}
|
||||
|
||||
func tapPasswordVisibilityToggle() {
|
||||
visibilityToggle.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapSignUp() {
|
||||
signUpButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func tapForgotPassword() {
|
||||
forgotPasswordButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
|
||||
func assertPasswordFieldVisible() {
|
||||
XCTAssertTrue(passwordVisibleField.waitForExistence(timeout: 5), "Expected visible password text field after toggle")
|
||||
}
|
||||
}
|
||||
|
||||
struct RegisterScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
private var usernameField: XCUIElement { app.textFields[UITestID.Auth.registerUsernameField] }
|
||||
private var emailField: XCUIElement { app.textFields[UITestID.Auth.registerEmailField] }
|
||||
private var passwordField: XCUIElement { app.secureTextFields[UITestID.Auth.registerPasswordField] }
|
||||
private var confirmPasswordField: XCUIElement { app.secureTextFields[UITestID.Auth.registerConfirmPasswordField] }
|
||||
private var registerButton: XCUIElement { app.buttons[UITestID.Auth.registerButton] }
|
||||
private var cancelButton: XCUIElement { app.buttons[UITestID.Auth.registerCancelButton] }
|
||||
|
||||
func waitForLoad(timeout: TimeInterval = 15) {
|
||||
usernameField.waitForExistenceOrFail(timeout: timeout)
|
||||
registerButton.waitForExistenceOrFail(timeout: timeout)
|
||||
}
|
||||
|
||||
func fill(username: String, email: String, password: String) {
|
||||
usernameField.waitForExistenceOrFail(timeout: 10)
|
||||
usernameField.forceTap()
|
||||
usernameField.typeText(username)
|
||||
|
||||
emailField.waitForExistenceOrFail(timeout: 10)
|
||||
emailField.forceTap()
|
||||
emailField.typeText(email)
|
||||
|
||||
passwordField.waitForExistenceOrFail(timeout: 10)
|
||||
passwordField.forceTap()
|
||||
passwordField.typeText(password)
|
||||
|
||||
confirmPasswordField.waitForExistenceOrFail(timeout: 10)
|
||||
confirmPasswordField.forceTap()
|
||||
confirmPasswordField.typeText(password)
|
||||
}
|
||||
|
||||
func tapCancel() {
|
||||
cancelButton.waitUntilHittable(timeout: 10).tap()
|
||||
}
|
||||
}
|
||||
47
iosApp/CaseraUITests/Framework/TestFlows.swift
Normal file
47
iosApp/CaseraUITests/Framework/TestFlows.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
import XCTest
|
||||
|
||||
enum TestFlows {
|
||||
@discardableResult
|
||||
static func navigateToLoginFromOnboarding(app: XCUIApplication) -> LoginScreen {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
welcome.tapAlreadyHaveAccount()
|
||||
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad()
|
||||
return login
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func navigateStartFreshToCreateAccount(
|
||||
app: XCUIApplication,
|
||||
residenceName: String = "UI Test Residence"
|
||||
) -> OnboardingCreateAccountScreen {
|
||||
let welcome = OnboardingWelcomeScreen(app: app)
|
||||
welcome.waitForLoad()
|
||||
welcome.tapStartFresh()
|
||||
|
||||
let valueProps = OnboardingValuePropsScreen(app: app)
|
||||
valueProps.waitForLoad()
|
||||
valueProps.tapContinue()
|
||||
|
||||
let nameResidence = OnboardingNameResidenceScreen(app: app)
|
||||
nameResidence.waitForLoad()
|
||||
nameResidence.enterResidenceName(residenceName)
|
||||
nameResidence.tapContinue()
|
||||
|
||||
let createAccount = OnboardingCreateAccountScreen(app: app)
|
||||
createAccount.waitForLoad()
|
||||
return createAccount
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func openRegisterFromLogin(app: XCUIApplication) -> RegisterScreen {
|
||||
let login = navigateToLoginFromOnboarding(app: app)
|
||||
login.tapSignUp()
|
||||
|
||||
let register = RegisterScreen(app: app)
|
||||
register.waitForLoad()
|
||||
return register
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user