Files
honeyDueKMP/iosApp/CaseraUITests/Framework/ScreenObjects.swift
treyt fc0e0688eb Add comprehensive iOS unit and UI test suites for greenfield test plan
- Create unit tests: DataLayerTests (27 tests for DATA-001–007), DataManagerExtendedTests
  (20 tests for TASK-005, TASK-012, TCOMP-003, THEME-001, QA-002), plus ValidationHelpers,
  TaskMetrics, StringExtensions, DoubleExtensions, DateUtils, DocumentHelpers, ErrorMessageParser
- Create UI tests: AuthenticationTests, PasswordResetTests, OnboardingTests, TaskIntegration,
  ContractorIntegration, ResidenceIntegration, DocumentIntegration, DataLayer, Stability
- Add UI test framework: AuthenticatedTestCase, ScreenObjects, TestFlows, TestAccountManager,
  TestAccountAPIClient, TestDataCleaner, TestDataSeeder
- Add accessibility identifiers to password reset views for UI test support
- Add greenfield test plan CSVs and update automated column for 27 test IDs
- All 297 unit tests pass across 60 suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:37:56 -06:00

412 lines
16 KiB
Swift

import XCTest
struct UITestID {
struct Root {
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 {
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 PasswordReset {
static let emailField = "PasswordReset.EmailField"
static let sendCodeButton = "PasswordReset.SendCodeButton"
static let backToLoginButton = "PasswordReset.BackToLoginButton"
static let codeField = "PasswordReset.CodeField"
static let verifyCodeButton = "PasswordReset.VerifyCodeButton"
static let resendCodeButton = "PasswordReset.ResendCodeButton"
static let newPasswordField = "PasswordReset.NewPasswordField"
static let confirmPasswordField = "PasswordReset.ConfirmPasswordField"
static let resetButton = "PasswordReset.ResetButton"
static let returnToLoginButton = "PasswordReset.ReturnToLoginButton"
}
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.waitForExistenceOrFail(timeout: 10)
if loginButton.isHittable {
loginButton.tap()
} else {
loginButton.forceTap()
}
}
}
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) {
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)
if !emailField.hasKeyboardFocus {
emailField.forceTap()
if !emailField.hasKeyboardFocus {
advanceToNextField()
emailField.forceTap()
}
}
emailField.typeText(email)
advanceToNextField()
passwordField.waitForExistenceOrFail(timeout: 10)
if !passwordField.hasKeyboardFocus {
passwordField.forceTap()
}
passwordField.typeText(password)
advanceToNextField()
confirmPasswordField.waitForExistenceOrFail(timeout: 10)
if !confirmPasswordField.hasKeyboardFocus {
confirmPasswordField.forceTap()
}
confirmPasswordField.typeText(password)
}
func tapCancel() {
cancelButton.waitUntilHittable(timeout: 10).tap()
}
}
// MARK: - Password Reset Screens
struct ForgotPasswordScreen {
let app: XCUIApplication
private var emailField: XCUIElement { app.textFields[UITestID.PasswordReset.emailField] }
private var sendCodeButton: XCUIElement { app.buttons[UITestID.PasswordReset.sendCodeButton] }
private var backToLoginButton: XCUIElement { app.buttons[UITestID.PasswordReset.backToLoginButton] }
func waitForLoad(timeout: TimeInterval = 15) {
// Wait for the email field or the "Forgot Password?" title
let emailLoaded = emailField.waitForExistence(timeout: timeout)
if !emailLoaded {
let title = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'Forgot Password'")
).firstMatch
XCTAssertTrue(title.waitForExistence(timeout: 5), "Expected forgot password screen to load")
}
}
func enterEmail(_ email: String) {
emailField.waitUntilHittable(timeout: 10).tap()
emailField.typeText(email)
}
func tapSendCode() {
sendCodeButton.waitUntilHittable(timeout: 10).tap()
}
func tapBackToLogin() {
backToLoginButton.waitUntilHittable(timeout: 10).tap()
}
}
struct VerifyResetCodeScreen {
let app: XCUIApplication
private var codeField: XCUIElement { app.textFields[UITestID.PasswordReset.codeField] }
private var verifyCodeButton: XCUIElement { app.buttons[UITestID.PasswordReset.verifyCodeButton] }
private var resendCodeButton: XCUIElement { app.buttons[UITestID.PasswordReset.resendCodeButton] }
func waitForLoad(timeout: TimeInterval = 15) {
let codeLoaded = codeField.waitForExistence(timeout: timeout)
if !codeLoaded {
let title = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'Check Your Email'")
).firstMatch
XCTAssertTrue(title.waitForExistence(timeout: 5), "Expected verify reset code screen to load")
}
}
func enterCode(_ code: String) {
codeField.waitUntilHittable(timeout: 10).tap()
codeField.typeText(code)
}
func tapVerify() {
verifyCodeButton.waitUntilHittable(timeout: 10).tap()
}
func tapResendCode() {
resendCodeButton.waitUntilHittable(timeout: 10).tap()
}
}
struct ResetPasswordScreen {
let app: XCUIApplication
// The new password field may be a SecureField or TextField depending on visibility toggle
private var newPasswordSecureField: XCUIElement { app.secureTextFields[UITestID.PasswordReset.newPasswordField] }
private var newPasswordVisibleField: XCUIElement { app.textFields[UITestID.PasswordReset.newPasswordField] }
private var confirmPasswordSecureField: XCUIElement { app.secureTextFields[UITestID.PasswordReset.confirmPasswordField] }
private var confirmPasswordVisibleField: XCUIElement { app.textFields[UITestID.PasswordReset.confirmPasswordField] }
private var resetButton: XCUIElement { app.buttons[UITestID.PasswordReset.resetButton] }
private var returnToLoginButton: XCUIElement { app.buttons[UITestID.PasswordReset.returnToLoginButton] }
func waitForLoad(timeout: TimeInterval = 15) {
let loaded = newPasswordSecureField.waitForExistence(timeout: timeout)
|| newPasswordVisibleField.waitForExistence(timeout: 3)
if !loaded {
let title = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'Set New Password'")
).firstMatch
XCTAssertTrue(title.waitForExistence(timeout: 5), "Expected reset password screen to load")
}
}
func enterNewPassword(_ password: String) {
if newPasswordSecureField.exists {
newPasswordSecureField.waitUntilHittable(timeout: 10).tap()
newPasswordSecureField.typeText(password)
} else {
newPasswordVisibleField.waitUntilHittable(timeout: 10).tap()
newPasswordVisibleField.typeText(password)
}
}
func enterConfirmPassword(_ password: String) {
if confirmPasswordSecureField.exists {
confirmPasswordSecureField.waitUntilHittable(timeout: 10).tap()
confirmPasswordSecureField.typeText(password)
} else {
confirmPasswordVisibleField.waitUntilHittable(timeout: 10).tap()
confirmPasswordVisibleField.typeText(password)
}
}
func tapReset() {
resetButton.waitUntilHittable(timeout: 10).tap()
}
func tapReturnToLogin() {
returnToLoginButton.waitUntilHittable(timeout: 10).tap()
}
var isResetButtonEnabled: Bool {
resetButton.waitForExistenceOrFail(timeout: 10)
return resetButton.isEnabled
}
}