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>
This commit is contained in:
215
iosApp/CaseraUITests/Tests/ContractorIntegrationTests.swift
Normal file
215
iosApp/CaseraUITests/Tests/ContractorIntegrationTests.swift
Normal file
@@ -0,0 +1,215 @@
|
||||
import XCTest
|
||||
|
||||
/// Integration tests for contractor CRUD against the real local backend.
|
||||
///
|
||||
/// Test Plan IDs: CON-002, CON-005, CON-006
|
||||
/// Data is seeded via API and cleaned up in tearDown.
|
||||
final class ContractorIntegrationTests: AuthenticatedTestCase {
|
||||
|
||||
override var useSeededAccount: Bool { true }
|
||||
|
||||
// MARK: - CON-002: Create Contractor
|
||||
|
||||
func testCON002_CreateContractorMinimalFields() {
|
||||
navigateToContractors()
|
||||
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Contractor.addButton]
|
||||
let emptyState = app.otherElements[AccessibilityIdentifiers.Contractor.emptyStateView]
|
||||
let contractorList = app.otherElements[AccessibilityIdentifiers.Contractor.contractorsList]
|
||||
|
||||
let loaded = addButton.waitForExistence(timeout: defaultTimeout)
|
||||
|| emptyState.waitForExistence(timeout: 3)
|
||||
|| contractorList.waitForExistence(timeout: 3)
|
||||
XCTAssertTrue(loaded, "Contractors screen should load")
|
||||
|
||||
if addButton.exists && addButton.isHittable {
|
||||
addButton.forceTap()
|
||||
} else {
|
||||
let emptyAddButton = app.buttons.containing(
|
||||
NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")
|
||||
).firstMatch
|
||||
emptyAddButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
emptyAddButton.forceTap()
|
||||
}
|
||||
|
||||
let nameField = app.textFields[AccessibilityIdentifiers.Contractor.nameField]
|
||||
nameField.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
let uniqueName = "IntTest Contractor \(Int(Date().timeIntervalSince1970))"
|
||||
nameField.forceTap()
|
||||
nameField.typeText(uniqueName)
|
||||
|
||||
let saveButton = app.buttons[AccessibilityIdentifiers.Contractor.saveButton]
|
||||
saveButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
saveButton.forceTap()
|
||||
|
||||
let newContractor = app.staticTexts[uniqueName]
|
||||
XCTAssertTrue(
|
||||
newContractor.waitForExistence(timeout: longTimeout),
|
||||
"Newly created contractor should appear in list"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - CON-005: Edit Contractor
|
||||
|
||||
func testCON005_EditContractor() {
|
||||
// Seed a contractor via API
|
||||
let contractor = cleaner.seedContractor(name: "Edit Target Contractor \(Int(Date().timeIntervalSince1970))")
|
||||
|
||||
navigateToContractors()
|
||||
|
||||
// Find and tap the seeded contractor
|
||||
let card = app.staticTexts[contractor.name]
|
||||
card.waitForExistenceOrFail(timeout: longTimeout)
|
||||
card.forceTap()
|
||||
|
||||
// Tap edit
|
||||
let editButton = app.buttons[AccessibilityIdentifiers.Contractor.editButton]
|
||||
editButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
editButton.forceTap()
|
||||
|
||||
// Update name
|
||||
let nameField = app.textFields[AccessibilityIdentifiers.Contractor.nameField]
|
||||
nameField.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
nameField.forceTap()
|
||||
nameField.press(forDuration: 1.0)
|
||||
let selectAll = app.menuItems["Select All"]
|
||||
if selectAll.waitForExistence(timeout: 2) {
|
||||
selectAll.tap()
|
||||
}
|
||||
|
||||
let updatedName = "Updated Contractor \(Int(Date().timeIntervalSince1970))"
|
||||
nameField.typeText(updatedName)
|
||||
|
||||
let saveButton = app.buttons[AccessibilityIdentifiers.Contractor.saveButton]
|
||||
saveButton.scrollIntoView(in: app.scrollViews.firstMatch)
|
||||
saveButton.forceTap()
|
||||
|
||||
let updatedText = app.staticTexts[updatedName]
|
||||
XCTAssertTrue(
|
||||
updatedText.waitForExistence(timeout: longTimeout),
|
||||
"Updated contractor name should appear after edit"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - CON-007: Favorite Toggle
|
||||
|
||||
func test20_toggleContractorFavorite() {
|
||||
// Seed a contractor via API and track it for cleanup
|
||||
let contractor = cleaner.seedContractor(name: "Favorite Toggle Contractor \(Int(Date().timeIntervalSince1970))")
|
||||
|
||||
navigateToContractors()
|
||||
|
||||
// Find and open the seeded contractor
|
||||
let card = app.staticTexts[contractor.name]
|
||||
card.waitForExistenceOrFail(timeout: longTimeout)
|
||||
card.forceTap()
|
||||
|
||||
// Look for a favorite / star button in the detail view.
|
||||
// The button may be labelled "Favorite", carry a star SF symbol, or use a toggle.
|
||||
let favoriteButton = app.buttons.containing(
|
||||
NSPredicate(format: "label CONTAINS[c] 'Favorite' OR label CONTAINS[c] 'Star' OR label CONTAINS[c] 'favourite'")
|
||||
).firstMatch
|
||||
|
||||
guard favoriteButton.waitForExistence(timeout: defaultTimeout) else {
|
||||
XCTFail("Favorite/star button not found on contractor detail view")
|
||||
return
|
||||
}
|
||||
|
||||
// Capture initial accessibility value / label to detect change
|
||||
let initialLabel = favoriteButton.label
|
||||
|
||||
// First toggle — mark as favourite
|
||||
favoriteButton.forceTap()
|
||||
|
||||
// Brief pause so the UI can settle after the API call
|
||||
_ = app.staticTexts.firstMatch.waitForExistence(timeout: 2)
|
||||
|
||||
// The button's label or selected state should have changed
|
||||
let afterFirstToggleLabel = favoriteButton.label
|
||||
XCTAssertNotEqual(
|
||||
initialLabel, afterFirstToggleLabel,
|
||||
"Favorite button appearance should change after first toggle"
|
||||
)
|
||||
|
||||
// Second toggle — un-mark as favourite, state should return to original
|
||||
favoriteButton.forceTap()
|
||||
|
||||
_ = app.staticTexts.firstMatch.waitForExistence(timeout: 2)
|
||||
|
||||
let afterSecondToggleLabel = favoriteButton.label
|
||||
XCTAssertEqual(
|
||||
initialLabel, afterSecondToggleLabel,
|
||||
"Favorite button appearance should return to original after second toggle"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - CON-008: Contractor by Residence Filter
|
||||
|
||||
func test21_contractorByResidenceFilter() throws {
|
||||
// Seed a residence and a contractor linked to it
|
||||
let residence = cleaner.seedResidence(name: "Filter Test Residence \(Int(Date().timeIntervalSince1970))")
|
||||
let contractor = cleaner.seedContractor(
|
||||
name: "Residence Contractor \(Int(Date().timeIntervalSince1970))",
|
||||
fields: ["residence_id": residence.id]
|
||||
)
|
||||
|
||||
navigateToResidences()
|
||||
|
||||
// Open the seeded residence's detail view
|
||||
let residenceText = app.staticTexts[residence.name]
|
||||
residenceText.waitForExistenceOrFail(timeout: longTimeout)
|
||||
residenceText.forceTap()
|
||||
|
||||
// Look for a Contractors section within the residence detail.
|
||||
// The section header text or accessibility element is checked first.
|
||||
let contractorsSectionHeader = app.staticTexts.containing(
|
||||
NSPredicate(format: "label CONTAINS[c] 'Contractor'")
|
||||
).firstMatch
|
||||
|
||||
guard contractorsSectionHeader.waitForExistence(timeout: defaultTimeout) else {
|
||||
throw XCTSkip("Residence detail does not expose a Contractors section — skipping filter test")
|
||||
}
|
||||
|
||||
// Verify the seeded contractor appears in the residence's contractor list
|
||||
let contractorEntry = app.staticTexts[contractor.name]
|
||||
XCTAssertTrue(
|
||||
contractorEntry.waitForExistence(timeout: defaultTimeout),
|
||||
"Contractor '\(contractor.name)' should appear in the contractors section of residence '\(residence.name)'"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - CON-006: Delete Contractor
|
||||
|
||||
func testCON006_DeleteContractor() {
|
||||
// Seed a contractor via API — don't track since we'll delete through UI
|
||||
let deleteName = "Delete Contractor \(Int(Date().timeIntervalSince1970))"
|
||||
TestDataSeeder.createContractor(token: session.token, name: deleteName)
|
||||
|
||||
navigateToContractors()
|
||||
|
||||
let target = app.staticTexts[deleteName]
|
||||
target.waitForExistenceOrFail(timeout: longTimeout)
|
||||
target.forceTap()
|
||||
|
||||
let deleteButton = app.buttons[AccessibilityIdentifiers.Contractor.deleteButton]
|
||||
deleteButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
||||
deleteButton.forceTap()
|
||||
|
||||
let confirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton]
|
||||
let alertDelete = app.alerts.buttons.containing(
|
||||
NSPredicate(format: "label CONTAINS[c] 'Delete' OR label CONTAINS[c] 'Confirm'")
|
||||
).firstMatch
|
||||
|
||||
if confirmButton.waitForExistence(timeout: shortTimeout) {
|
||||
confirmButton.tap()
|
||||
} else if alertDelete.waitForExistence(timeout: shortTimeout) {
|
||||
alertDelete.tap()
|
||||
}
|
||||
|
||||
let deletedContractor = app.staticTexts[deleteName]
|
||||
XCTAssertTrue(
|
||||
deletedContractor.waitForNonExistence(timeout: longTimeout),
|
||||
"Deleted contractor should no longer appear"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user