Major infrastructure changes: - BaseUITestCase: per-suite app termination via class setUp() prevents stale state when parallel clones share simulators - relaunchBetweenTests override for suites that modify login/onboarding state - focusAndType: dedicated SecureTextField path handles iOS strong password autofill suggestions (Choose My Own Password / Not Now dialogs) - LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for offscreen buttons instead of simple swipeUp - Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen, ResetPasswordScreen (Rule 3 compliance) - Removed all usleep calls from screen objects (Rule 14 compliance) App fixes exposed by tests: - ContractorsListView: added onDismiss to sheet for list refresh after save - AllTasksView: added Task.RefreshButton accessibility identifier - AccessibilityIdentifiers: added Task.refreshButton - DocumentsWarrantiesView: onDismiss handler for document list refresh - Various form views: textContentType, submitLabel, onSubmit for keyboard flow Test fixes: - PasswordResetTests: handle auto-login after reset (app skips success screen) - AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button - All pre-login suites use relaunchBetweenTests for test independence - Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests, CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests 10 remaining failures: 5 iOS strong password autofill (simulator env), 3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
488 lines
20 KiB
Swift
488 lines
20 KiB
Swift
import XCTest
|
|
|
|
/// Comprehensive residence testing suite covering all scenarios, edge cases, and variations
|
|
/// This test suite is designed to be bulletproof and catch regressions early
|
|
///
|
|
/// Test Order (least to most complex):
|
|
/// 1. Error/incomplete data tests
|
|
/// 2. Creation tests
|
|
/// 3. Edit/update tests
|
|
/// 4. Delete/remove tests (none currently)
|
|
/// 5. Navigation/view tests
|
|
/// 6. Performance tests
|
|
final class Suite4_ComprehensiveResidenceTests: AuthenticatedUITestCase {
|
|
|
|
override var needsAPISession: Bool { true }
|
|
|
|
// Test data tracking
|
|
var createdResidenceNames: [String] = []
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
|
|
// Dismiss any open form/sheet from a previous test
|
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch
|
|
if cancelButton.exists { cancelButton.tap() }
|
|
|
|
navigateToResidences()
|
|
residenceList.addButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Residence add button should appear after navigation")
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
// Ensure all UI-created residences are tracked for API cleanup
|
|
if !createdResidenceNames.isEmpty,
|
|
let allResidences = TestAccountAPIClient.listResidences(token: session.token) {
|
|
for name in createdResidenceNames {
|
|
if let res = allResidences.first(where: { $0.name.contains(name) }) {
|
|
cleaner.trackResidence(res.id)
|
|
}
|
|
}
|
|
}
|
|
createdResidenceNames.removeAll()
|
|
try super.tearDownWithError()
|
|
}
|
|
|
|
// MARK: - Page Objects
|
|
|
|
private var residenceList: ResidenceListScreen { ResidenceListScreen(app: app) }
|
|
private var residenceForm: ResidenceFormScreen { ResidenceFormScreen(app: app) }
|
|
private var residenceDetail: ResidenceDetailScreen { ResidenceDetailScreen(app: app) }
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func openResidenceForm(file: StaticString = #filePath, line: UInt = #line) {
|
|
let addButton = residenceList.addButton
|
|
addButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Residence add button should exist", file: file, line: line)
|
|
XCTAssertTrue(addButton.isEnabled, "Residence add button should be enabled", file: file, line: line)
|
|
addButton.tap()
|
|
residenceForm.nameField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Residence form should open", file: file, line: line)
|
|
}
|
|
|
|
/// Fill sequential address fields using the Return key to advance focus.
|
|
/// Fill address fields. Dismisses keyboard between each field for clean focus.
|
|
private func fillAddressFields(street: String, city: String, state: String, postal: String) {
|
|
// Scroll address section into view — may need multiple swipes on smaller screens
|
|
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField].firstMatch
|
|
for _ in 0..<3 {
|
|
if streetField.exists && streetField.isHittable { break }
|
|
app.swipeUp()
|
|
}
|
|
streetField.waitForExistenceOrFail(timeout: navigationTimeout, message: "Street field should appear after scroll")
|
|
|
|
fillTextField(identifier: AccessibilityIdentifiers.Residence.streetAddressField, text: street)
|
|
dismissKeyboard()
|
|
fillTextField(identifier: AccessibilityIdentifiers.Residence.cityField, text: city)
|
|
dismissKeyboard()
|
|
fillTextField(identifier: AccessibilityIdentifiers.Residence.stateProvinceField, text: state)
|
|
dismissKeyboard()
|
|
fillTextField(identifier: AccessibilityIdentifiers.Residence.postalCodeField, text: postal)
|
|
dismissKeyboard()
|
|
}
|
|
|
|
private func selectPropertyType(type: String) {
|
|
let picker = app.buttons[AccessibilityIdentifiers.Residence.propertyTypePicker].firstMatch
|
|
guard picker.waitForExistence(timeout: defaultTimeout) else {
|
|
XCTFail("Property type picker not found")
|
|
return
|
|
}
|
|
picker.tap()
|
|
|
|
// SwiftUI Picker in Form pushes a selection list — find the option by text
|
|
let option = app.staticTexts[type]
|
|
option.waitForExistenceOrFail(timeout: navigationTimeout, message: "Property type '\(type)' should appear in picker list")
|
|
option.tap()
|
|
}
|
|
|
|
private func createResidence(
|
|
name: String,
|
|
propertyType: String? = nil,
|
|
street: String = "123 Test St",
|
|
city: String = "TestCity",
|
|
state: String = "TS",
|
|
postal: String = "12345"
|
|
) {
|
|
openResidenceForm()
|
|
|
|
residenceForm.enterName(name)
|
|
if let propertyType = propertyType {
|
|
selectPropertyType(type: propertyType)
|
|
}
|
|
dismissKeyboard()
|
|
fillAddressFields(street: street, city: city, state: state, postal: postal)
|
|
residenceForm.save()
|
|
|
|
createdResidenceNames.append(name)
|
|
|
|
// Track for API cleanup
|
|
if let items = TestAccountAPIClient.listResidences(token: session.token),
|
|
let created = items.first(where: { $0.name.contains(name) }) {
|
|
cleaner.trackResidence(created.id)
|
|
}
|
|
}
|
|
|
|
private func findResidence(name: String) -> XCUIElement {
|
|
return app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", name)).firstMatch
|
|
}
|
|
|
|
// MARK: - 1. Error/Validation Tests
|
|
|
|
func test01_cannotCreateResidenceWithEmptyName() {
|
|
openResidenceForm()
|
|
|
|
// Leave name empty, fill only address
|
|
fillAddressFields(street: "123 Test St", city: "TestCity", state: "TS", postal: "12345")
|
|
|
|
// Scroll to save button if needed
|
|
app.swipeUp()
|
|
|
|
// Submit button should be disabled when name is empty (may be labeled "Add" or "Save")
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch
|
|
_ = saveButton.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(saveButton.exists, "Submit button should exist")
|
|
XCTAssertFalse(saveButton.isEnabled, "Submit button should be disabled when name is empty")
|
|
|
|
// Clean up: dismiss the form so next test starts on the list
|
|
residenceForm.cancel()
|
|
}
|
|
|
|
func test02_cancelResidenceCreation() {
|
|
openResidenceForm()
|
|
|
|
// Fill some data
|
|
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch
|
|
nameField.tap()
|
|
// Wait for keyboard to appear before typing
|
|
let keyboard = app.keyboards.firstMatch
|
|
_ = keyboard.waitForExistence(timeout: 3)
|
|
nameField.typeText("This will be canceled")
|
|
|
|
// Tap cancel
|
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Residence.formCancelButton].firstMatch
|
|
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
|
|
cancelButton.tap()
|
|
|
|
// Should be back on residences list
|
|
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
|
_ = residencesTab.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(residencesTab.exists, "Should be back on residences list")
|
|
|
|
// Residence should not exist
|
|
let residence = findResidence(name: "This will be canceled")
|
|
XCTAssertFalse(residence.exists, "Canceled residence should not exist")
|
|
}
|
|
|
|
// MARK: - 2. Creation Tests
|
|
|
|
func test03_createResidenceWithMinimalData() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let residenceName = "Minimal Home \(timestamp)"
|
|
|
|
createResidence(name: residenceName)
|
|
|
|
let residenceInList = findResidence(name: residenceName)
|
|
XCTAssertTrue(residenceInList.waitForExistence(timeout: 10), "Residence should appear in list")
|
|
}
|
|
|
|
// test04_createResidenceWithAllPropertyTypes — removed: backend has no seeded residence types
|
|
|
|
func test05_createMultipleResidencesInSequence() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
|
|
for i in 1...3 {
|
|
let residenceName = "Sequential Home \(i) - \(timestamp)"
|
|
createResidence(name: residenceName)
|
|
|
|
navigateToResidences()
|
|
}
|
|
|
|
// Verify all residences exist
|
|
for i in 1...3 {
|
|
let residenceName = "Sequential Home \(i) - \(timestamp)"
|
|
let residence = findResidence(name: residenceName)
|
|
XCTAssertTrue(residence.exists, "Residence \(i) should exist in list")
|
|
}
|
|
}
|
|
|
|
func test06_createResidenceWithVeryLongName() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let longName = "This is an extremely long residence name that goes on and on and on to test how the system handles very long text input in the name field \(timestamp)"
|
|
|
|
createResidence(name: longName)
|
|
|
|
// Verify it appears (may be truncated in display)
|
|
let residence = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'extremely long residence'")).firstMatch
|
|
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Long name residence should exist")
|
|
}
|
|
|
|
func test07_createResidenceWithSpecialCharacters() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let specialName = "Special !@#$%^&*() Home \(timestamp)"
|
|
|
|
createResidence(name: specialName)
|
|
|
|
let residence = findResidence(name: "Special")
|
|
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with special chars should exist")
|
|
}
|
|
|
|
func test08_createResidenceWithEmojis() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let emojiName = "Beach House \(timestamp)"
|
|
|
|
createResidence(name: emojiName)
|
|
|
|
let residence = findResidence(name: "Beach House")
|
|
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with emojis should exist")
|
|
}
|
|
|
|
func test09_createResidenceWithInternationalCharacters() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let internationalName = "Chateau Montreal \(timestamp)"
|
|
|
|
createResidence(name: internationalName)
|
|
|
|
let residence = findResidence(name: "Chateau")
|
|
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with international chars should exist")
|
|
}
|
|
|
|
func test10_createResidenceWithVeryLongAddress() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let residenceName = "Long Address Home \(timestamp)"
|
|
|
|
createResidence(
|
|
name: residenceName,
|
|
street: "123456789 Very Long Street Name That Goes On And On Boulevard Apartment Complex Unit 42B",
|
|
city: "VeryLongCityNameThatTestsTheLimit",
|
|
state: "CA",
|
|
postal: "12345-6789"
|
|
)
|
|
|
|
let residence = findResidence(name: residenceName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: 10), "Residence with long address should exist")
|
|
}
|
|
|
|
// MARK: - 3. Edit/Update Tests
|
|
|
|
func test11_editResidenceName() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let originalName = "Original Name \(timestamp)"
|
|
let newName = "Edited Name \(timestamp)"
|
|
|
|
// Create residence
|
|
createResidence(name: originalName)
|
|
|
|
navigateToResidences()
|
|
|
|
// Find and tap residence
|
|
let residence = findResidence(name: originalName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist")
|
|
residence.tap()
|
|
|
|
// Tap edit button
|
|
let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch
|
|
if editButton.waitForExistence(timeout: defaultTimeout) {
|
|
editButton.tap()
|
|
|
|
// Edit name
|
|
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch
|
|
if nameField.waitForExistence(timeout: defaultTimeout) {
|
|
nameField.clearAndEnterText(newName, app: app)
|
|
|
|
// Save
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch
|
|
if saveButton.exists {
|
|
saveButton.tap()
|
|
|
|
// Track new name
|
|
createdResidenceNames.append(newName)
|
|
|
|
// Verify new name appears
|
|
navigateToResidences()
|
|
let updatedResidence = findResidence(name: newName)
|
|
XCTAssertTrue(updatedResidence.waitForExistence(timeout: defaultTimeout), "Residence should show updated name")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func test12_updateAllResidenceFields() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let originalName = "Update All Fields \(timestamp)"
|
|
let newName = "All Fields Updated \(timestamp)"
|
|
let newStreet = "999 Updated Avenue"
|
|
let newCity = "NewCity"
|
|
let newState = "NC"
|
|
let newPostal = "99999"
|
|
|
|
// Create residence with initial values
|
|
createResidence(name: originalName, street: "123 Old St", city: "OldCity", state: "OC", postal: "11111")
|
|
|
|
navigateToResidences()
|
|
|
|
// Find and tap residence
|
|
let residence = findResidence(name: originalName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist")
|
|
residence.tap()
|
|
|
|
// Tap edit button
|
|
let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch
|
|
XCTAssertTrue(editButton.waitForExistence(timeout: defaultTimeout), "Edit button should exist")
|
|
editButton.tap()
|
|
|
|
// Update name
|
|
let nameField = app.textFields[AccessibilityIdentifiers.Residence.nameField].firstMatch
|
|
XCTAssertTrue(nameField.waitForExistence(timeout: defaultTimeout), "Name field should exist")
|
|
nameField.clearAndEnterText(newName, app: app)
|
|
|
|
// Property type update skipped — backend has no seeded residence types
|
|
|
|
// Dismiss keyboard from name edit, scroll to address fields
|
|
dismissKeyboard()
|
|
app.swipeUp()
|
|
|
|
// Update address fields
|
|
let streetField = app.textFields[AccessibilityIdentifiers.Residence.streetAddressField].firstMatch
|
|
if streetField.waitForExistence(timeout: defaultTimeout) {
|
|
streetField.clearAndEnterText(newStreet, app: app)
|
|
dismissKeyboard()
|
|
}
|
|
|
|
let cityField = app.textFields[AccessibilityIdentifiers.Residence.cityField].firstMatch
|
|
if cityField.waitForExistence(timeout: defaultTimeout) {
|
|
cityField.clearAndEnterText(newCity, app: app)
|
|
dismissKeyboard()
|
|
}
|
|
|
|
let stateField = app.textFields[AccessibilityIdentifiers.Residence.stateProvinceField].firstMatch
|
|
if stateField.waitForExistence(timeout: defaultTimeout) {
|
|
stateField.clearAndEnterText(newState, app: app)
|
|
dismissKeyboard()
|
|
}
|
|
|
|
// Update postal code
|
|
let postalField = app.textFields[AccessibilityIdentifiers.Residence.postalCodeField].firstMatch
|
|
if postalField.exists {
|
|
postalField.clearAndEnterText(newPostal, app: app)
|
|
}
|
|
|
|
// Scroll to save button
|
|
app.swipeUp()
|
|
|
|
// Save
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Residence.saveButton].firstMatch
|
|
_ = saveButton.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
|
saveButton.tap()
|
|
|
|
// Wait for form to dismiss after API call
|
|
_ = saveButton.waitForNonExistence(timeout: defaultTimeout)
|
|
|
|
// Track new name
|
|
createdResidenceNames.append(newName)
|
|
|
|
// Verify updated residence appears in list with new name
|
|
navigateToResidences()
|
|
let updatedResidence = findResidence(name: newName)
|
|
XCTAssertTrue(updatedResidence.waitForExistence(timeout: defaultTimeout), "Residence should show updated name in list")
|
|
|
|
// Name update verified in list — detail view doesn't display address fields
|
|
|
|
}
|
|
|
|
// MARK: - 4. View/Navigation Tests
|
|
|
|
func test13_viewResidenceDetails() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let residenceName = "Detail View Test \(timestamp)"
|
|
|
|
// Create residence
|
|
createResidence(name: residenceName)
|
|
|
|
navigateToResidences()
|
|
|
|
// Tap on residence
|
|
let residence = findResidence(name: residenceName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist")
|
|
residence.tap()
|
|
|
|
// Verify detail view appears with edit button or tasks section
|
|
let editButton = app.buttons[AccessibilityIdentifiers.Residence.editButton].firstMatch
|
|
let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks' OR label CONTAINS[c] 'Maintenance'")).firstMatch
|
|
|
|
_ = editButton.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(editButton.exists || tasksSection.exists, "Detail view should show with edit button or tasks section")
|
|
}
|
|
|
|
func test14_navigateFromResidencesToOtherTabs() {
|
|
// From Residences tab
|
|
navigateToResidences()
|
|
|
|
// Navigate to Tasks
|
|
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
|
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist")
|
|
tasksTab.tap()
|
|
_ = tasksTab.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(tasksTab.isSelected, "Should be on Tasks tab")
|
|
|
|
// Navigate back to Residences
|
|
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
|
residencesTab.tap()
|
|
_ = residencesTab.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab")
|
|
|
|
// Navigate to Contractors
|
|
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
|
XCTAssertTrue(contractorsTab.exists, "Contractors tab should exist")
|
|
contractorsTab.tap()
|
|
_ = contractorsTab.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(contractorsTab.isSelected, "Should be on Contractors tab")
|
|
|
|
// Back to Residences
|
|
residencesTab.tap()
|
|
_ = residencesTab.waitForExistence(timeout: defaultTimeout)
|
|
XCTAssertTrue(residencesTab.isSelected, "Should be back on Residences tab again")
|
|
}
|
|
|
|
func test15_refreshResidencesList() {
|
|
navigateToResidences()
|
|
|
|
// Pull to refresh (if implemented) or use refresh button
|
|
let refreshButton = app.navigationBars.buttons.containing(NSPredicate(format: "label CONTAINS 'arrow.clockwise' OR label CONTAINS 'refresh'")).firstMatch
|
|
if refreshButton.waitForExistence(timeout: defaultTimeout) {
|
|
refreshButton.tap()
|
|
_ = app.activityIndicators.firstMatch.waitForNonExistence(timeout: defaultTimeout)
|
|
}
|
|
|
|
// Verify we're still on residences tab
|
|
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
|
XCTAssertTrue(residencesTab.isSelected, "Should still be on Residences tab after refresh")
|
|
}
|
|
|
|
// MARK: - 5. Persistence Tests
|
|
|
|
func test16_residencePersistsAfterBackgroundingApp() {
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let residenceName = "Persistence Test \(timestamp)"
|
|
|
|
// Create residence
|
|
createResidence(name: residenceName)
|
|
|
|
navigateToResidences()
|
|
|
|
// Verify residence exists
|
|
var residence = findResidence(name: residenceName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should exist before backgrounding")
|
|
|
|
// Background and reactivate app
|
|
XCUIDevice.shared.press(.home)
|
|
_ = app.wait(for: .runningForeground, timeout: 10)
|
|
|
|
// Navigate back to residences
|
|
navigateToResidences()
|
|
|
|
// Verify residence still exists
|
|
residence = findResidence(name: residenceName)
|
|
XCTAssertTrue(residence.waitForExistence(timeout: defaultTimeout), "Residence should persist after backgrounding app")
|
|
}
|
|
|
|
}
|