Files
honeyDueKMP/iosApp/HoneyDueUITests/Document/DocumentWarrantyUITests.swift
T
Trey T c52ce4d497 Re-architect iOS XCUITest suite: per-test isolation + domain organization
Migrate the XCUITest suite off the legacy shared-account model (and the
prior Django-style auth assumptions) to a parallel-safe, domain-organized
architecture, validated end-to-end against the live Kratos stack.

Isolation (parallel-safe by construction):
- Core/Fixtures/TestAccount.swift: each test mints its own pre-verified
  Kratos identity (uit_<domain>_<uuid>@test.honeydue.local), logs in, seeds
  under its own token, and deletes the identity in teardown (cascading all
  data + clearing Kratos). No shared testuser; parallel workers no longer race.
- AuthenticatedUITestCase rewritten to that model (member surface preserved);
  adds requiresResidence / seedAccountPreconditions to seed UI-gated data
  BEFORE login (a fresh account is empty at login).

Organization (255 tests preserved, none dropped):
- 21 domain suites under Auth/ Onboarding/ Residence/ Task/ Contractor/
  Document/ Sharing/ Navigation/ Smoke/ CrossCutting/ E2E/, consistent
  <Domain>UITests naming. Removes the Suite1..11 / AAA_ / ZZ_ / Tests/Rebuild
  naming chaos and the overlapping task/residence/auth suites.

Runner + test plans:
- run_ui_tests.sh: Smoke gate -> Seed -> Parallel(8 workers) -> Sweep. The
  parallel phase runs the whole target minus phase-managed suites via
  -skip-testing, so new suites auto-include (no hand-maintained list to drift).
  Drops the 2-worker cap and Suite6 isolation (isolation made them moot).
- HoneyDueUITests.xctestplan skips the 4 phase-managed suites; adds Smoke.xctestplan.

Kratos auth fixes folded in (login/verify/reset endpoints removed under Kratos):
real Mailpit verification codes replace the obsolete fixed "123456"; teardown
deletes Kratos identities; admin-panel login uses the correct seeded password.

Build green; isolation, parallelism, and the precondition/sharing migrations
validated against the live stack (0 leaked accounts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:26:50 -05:00

537 lines
22 KiB
Swift

import XCTest
/// Document warranty UI test suite (warranty-specific lifecycle and filters).
///
/// Holds the warranty-aspect tests split out from the former
/// `Suite8_DocumentWarrantyTests`: warranty creation (all fields / future /
/// expired / special chars), warranty detail with dates, warranty edit, warranty
/// delete, category + active-only filters, the empty-warranties state, and the
/// combined-filters scenario. The generic document CRUD tests live in
/// `DocumentCRUDUITests`.
///
/// Warranties are created through the document form which gates on a residence,
/// so `requiresResidence = true` seeds one "Precondition Home" residence before
/// login (the app loads it on its post-login fetch). All tests here create their
/// warranty through the UI, so no pre-seeded document is needed.
final class DocumentWarrantyUITests: AuthenticatedUITestCase {
override var requiresResidence: Bool { true }
// MARK: - Page Objects
private var docList: DocumentListScreen { DocumentListScreen(app: app) }
private var docForm: DocumentFormScreen { DocumentFormScreen(app: app) }
// MARK: - Helpers
/// Navigate to the Documents tab, load residence data into the DataManager
/// cache (so the property picker is populated), and prime the form once.
private func prepareDocumentsScreen() {
// Visit Residences tab to load residence data into DataManager cache
navigateToResidences()
pullToRefresh()
// Navigate to the Documents tab
navigateToDocuments()
// Open and close the document form once to prime the DataManager cache
// so the property picker is populated on subsequent opens.
let warmupAddButton = docList.addButton
if warmupAddButton.exists && warmupAddButton.isEnabled {
warmupAddButton.tap()
_ = docForm.titleField.waitForExistence(timeout: defaultTimeout)
cancelForm()
}
}
private func openDocumentForm(file: StaticString = #filePath, line: UInt = #line) {
let addButton = docList.addButton
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add button should exist and be enabled", file: file, line: line)
addButton.tap()
docForm.titleField.waitForExistenceOrFail(timeout: navigationTimeout, message: "Document form should appear", file: file, line: line)
}
private func fillTextEditor(text: String) {
let textEditor = app.textViews.firstMatch
if textEditor.exists {
textEditor.focusAndType(text, app: app)
}
}
/// Select a property from the residence picker. Fails the test if picker is missing or empty.
private func selectProperty(file: StaticString = #filePath, line: UInt = #line) {
// Look up the seeded residence name so we can match it by text in
// whichever picker variant iOS renders (menu, list, or wheel).
let residences = TestAccountAPIClient.listResidences(token: session.token) ?? []
let residenceName = residences.first?.name
let pickerButton = app.buttons[AccessibilityIdentifiers.Document.residencePicker].firstMatch
pickerButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Property picker should exist", file: file, line: line)
pickerButton.tap()
// Fast path: the residence option is often rendered as a plain Button
// or StaticText whose label is the residence name itself.
if let name = residenceName {
let byButton = app.buttons[name].firstMatch
if byButton.waitForExistence(timeout: 3) && byButton.isHittable {
byButton.tap()
_ = docForm.titleField.waitForExistence(timeout: navigationTimeout)
return
}
let byText = app.staticTexts[name].firstMatch
if byText.exists && byText.isHittable {
byText.tap()
_ = docForm.titleField.waitForExistence(timeout: navigationTimeout)
return
}
}
// SwiftUI Picker in Form renders either a menu (iOS 18+ default) or a
// pushed selection list.
let menuItem = app.menuItems.firstMatch
if menuItem.waitForExistence(timeout: 5) {
let allItems = app.menuItems.allElementsBoundByIndex
let target = allItems.last ?? menuItem
if target.isHittable {
target.tap()
} else {
target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
_ = app.menuItems.firstMatch.waitForNonExistence(timeout: 2)
return
} else {
// List-style picker find a cell/row with a residence name.
let cells = app.cells
guard cells.firstMatch.waitForExistence(timeout: navigationTimeout) else {
XCTFail("No residence options appeared in picker", file: file, line: line)
return
}
let hittable = NSPredicate(format: "isHittable == true")
for attempt in 0..<5 {
let targetCell = cells.count > 1 ? cells.element(boundBy: 1) : cells.element(boundBy: 0)
guard targetCell.exists else {
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
continue
}
_ = XCTWaiter().wait(
for: [XCTNSPredicateExpectation(predicate: hittable, object: targetCell)],
timeout: 2.0 + Double(attempt)
)
if targetCell.isHittable {
targetCell.tap()
if docForm.titleField.waitForExistence(timeout: 2) { break }
}
if docForm.titleField.exists, attempt < 4, pickerButton.exists, pickerButton.isHittable {
pickerButton.tap()
_ = cells.firstMatch.waitForExistence(timeout: 3)
}
}
}
// Wait for picker to dismiss and return to form
_ = docForm.titleField.waitForExistence(timeout: navigationTimeout)
}
private func submitForm(file: StaticString = #filePath, line: UInt = #line) {
// Dismiss keyboard by tapping outside form fields
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.15)).tap()
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 3)
// If keyboard still showing (can happen with long text / autocorrect), try Return key
if app.keyboards.firstMatch.exists {
app.typeText("\n")
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
}
let submitButton = docForm.saveButton
if !submitButton.exists || !submitButton.isHittable {
app.swipeUp()
_ = submitButton.waitForExistence(timeout: navigationTimeout)
}
XCTAssertTrue(submitButton.exists && submitButton.isEnabled, "Submit button should exist and be enabled", file: file, line: line)
if submitButton.isHittable {
submitButton.tap()
} else {
submitButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
if !submitButton.waitForNonExistence(timeout: loginTimeout) && submitButton.exists {
submitButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
_ = submitButton.waitForNonExistence(timeout: loginTimeout)
}
}
/// Look up a just-created document by title and track it for API cleanup.
private func trackDocumentForCleanup(title: String) {
if let items = TestAccountAPIClient.listDocuments(token: session.token),
let created = items.first(where: { $0.title.contains(title) }) {
cleaner.trackDocument(created.id)
}
}
private func cancelForm() {
let cancelButton = docForm.cancelButton
if cancelButton.exists {
cancelButton.tap()
_ = cancelButton.waitForNonExistence(timeout: defaultTimeout)
}
}
private func switchToWarrantiesTab() {
let warrantiesButton = app.buttons["Warranties"].firstMatch
if warrantiesButton.waitForExistence(timeout: navigationTimeout) {
warrantiesButton.tap()
return
}
// Fallback: segmented control button
app.segmentedControls.buttons["Warranties"].firstMatch.tap()
}
private func searchFor(text: String) {
let searchField = app.searchFields.firstMatch
if searchField.exists {
searchField.focusAndType(text, app: app)
// Wait for search results to settle
_ = app.cells.firstMatch.waitForExistence(timeout: defaultTimeout)
}
}
private func clearSearch() {
let searchField = app.searchFields.firstMatch
if searchField.exists {
let clearButton = searchField.buttons["Clear text"]
if clearButton.exists {
clearButton.tap()
}
}
}
@discardableResult
private func applyFilter(filterName: String) -> Bool {
// Open filter menu via accessibility identifier
let filterButton = app.buttons[AccessibilityIdentifiers.Common.filterButton].firstMatch
guard filterButton.waitForExistence(timeout: defaultTimeout) else { return false }
filterButton.forceTap()
// Select filter option
let filterOption = app.buttons[filterName]
if filterOption.waitForExistence(timeout: defaultTimeout) {
filterOption.forceTap()
_ = filterOption.waitForNonExistence(timeout: defaultTimeout)
return true
}
// Try as static text (some menus render options as text)
let filterText = app.staticTexts[filterName]
if filterText.exists {
filterText.forceTap()
_ = filterText.waitForNonExistence(timeout: defaultTimeout)
return true
}
return false
}
private func toggleActiveFilter() {
// The active filter toggle is a toolbar button with a checkmark.circle icon.
// No dedicated accessibility identifier exists; match by icon label.
let filled = app.buttons["checkmark.circle.fill"].firstMatch
let outline = app.buttons["checkmark.circle"].firstMatch
if filled.exists {
filled.tap()
} else if outline.exists {
outline.tap()
}
// Wait for filter results to update
_ = app.cells.firstMatch.waitForExistence(timeout: defaultTimeout)
}
// MARK: - Warranty Creation Tests
func test06_CreateWarrantyWithAllFields() {
prepareDocumentsScreen()
switchToWarrantiesTab()
openDocumentForm()
let testTitle = "Test Warranty \(UUID().uuidString.prefix(8))"
// Fill all warranty fields (including required fields)
selectProperty()
docForm.titleField.focusAndType(testTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Dishwasher") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Bosch") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.modelNumberField, text: "SHPM65Z55N")
fillTextField(identifier: AccessibilityIdentifiers.Document.serialNumberField, text: "SN123456789")
fillTextField(identifier: AccessibilityIdentifiers.Document.providerContactField, text: "1-800-BOSCH-00")
fillTextEditor(text: "Full warranty coverage for 2 years")
// Select dates
submitForm()
// Verify warranty appears
let warrantyCard = app.staticTexts[testTitle]
XCTAssertTrue(warrantyCard.waitForExistence(timeout: navigationTimeout), "Created warranty should appear in list")
}
func test07_CreateWarrantyWithFutureDates() {
prepareDocumentsScreen()
switchToWarrantiesTab()
openDocumentForm()
let testTitle = "Future Warranty \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(testTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Air Conditioner") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Carrier HVAC") // REQUIRED
// Set start date in future
submitForm()
let warrantyCard = app.staticTexts[testTitle]
XCTAssertTrue(warrantyCard.waitForExistence(timeout: defaultTimeout), "Warranty with future dates should be created")
}
func test08_CreateExpiredWarranty() {
prepareDocumentsScreen()
switchToWarrantiesTab()
openDocumentForm()
let testTitle = "Expired Warranty \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(testTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Water Heater") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "AO Smith") // REQUIRED
// Set dates in the past
submitForm()
// Expired warranty might not show with active filter on
// Toggle active filter off to see it
toggleActiveFilter()
let warrantyCard = app.staticTexts[testTitle]
XCTAssertTrue(warrantyCard.exists, "Expired warranty should be created and visible when filter is off")
}
// MARK: - Search and Filter Tests (warranty-side)
func test10_FilterWarrantiesByCategory() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Apply category filter if filter button is not found, the test
// still passes (verifies no crash). Only assert when the filter was applied.
let filterApplied = applyFilter(filterName: "Appliances")
if filterApplied {
// Should show filter chip or indication
let filterChip = app.staticTexts["Appliances"]
XCTAssertTrue(filterChip.exists || app.buttons["Appliances"].exists, "Should show active category filter")
// Clear filter
applyFilter(filterName: "All Categories")
}
// If filter was not applied (button not found), test passes no crash happened
}
func test12_ToggleActiveWarrantiesFilter() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Toggle active filter off
toggleActiveFilter()
// Toggle it back on
toggleActiveFilter()
// Should not crash
let warrantiesTab = app.buttons["Warranties"]
XCTAssertTrue(warrantiesTab.exists, "Active filter toggle should work without crashing")
}
// MARK: - Warranty Detail Tests
func test14_ViewWarrantyDetailWithDates() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Create a warranty
openDocumentForm()
let testTitle = "Warranty Detail Test \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(testTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Test Appliance") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Test Company") // REQUIRED
submitForm()
// Tap on warranty
let warrantyCard = app.staticTexts[testTitle]
XCTAssertTrue(warrantyCard.waitForExistence(timeout: navigationTimeout), "Warranty should exist")
warrantyCard.tap()
// Should show warranty details with dates
let detailScreen = app.staticTexts[testTitle]
XCTAssertTrue(detailScreen.waitForExistence(timeout: defaultTimeout), "Should show warranty detail")
// Date information not checked warranty was created without dates
// Go back
app.navigationBars.buttons.firstMatch.tap()
}
// MARK: - Edit Tests (warranty-side)
func test16_EditWarrantyDates() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Create warranty
openDocumentForm()
let testTitle = "Edit Dates Warranty \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(testTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "TV") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Samsung") // REQUIRED
submitForm()
// Open and edit
let warrantyCard = app.staticTexts[testTitle]
XCTAssertTrue(warrantyCard.waitForExistence(timeout: navigationTimeout), "Warranty should exist")
warrantyCard.tap()
let editButton = app.buttons[AccessibilityIdentifiers.Document.editButton].firstMatch
if editButton.waitForExistence(timeout: defaultTimeout) {
editButton.tap()
// Wait for edit form to load
let editTitleField = app.textFields[AccessibilityIdentifiers.Document.titleField].firstMatch
_ = editTitleField.waitForExistence(timeout: defaultTimeout)
// Change end date to extend warranty
submitForm()
}
app.navigationBars.buttons.element(boundBy: 0).tap()
}
// MARK: - Delete Tests (warranty-side)
func test18_DeleteWarranty() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Create warranty to delete
openDocumentForm()
let deleteTitle = "Warranty to Delete \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(deleteTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Test Item") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Test Provider") // REQUIRED
submitForm()
// Open and delete
let warrantyCard = app.staticTexts[deleteTitle]
XCTAssertTrue(warrantyCard.waitForExistence(timeout: navigationTimeout), "Warranty should exist")
warrantyCard.tap()
let deleteButton = app.buttons[AccessibilityIdentifiers.Document.deleteButton].firstMatch
if deleteButton.waitForExistence(timeout: defaultTimeout) {
deleteButton.tap()
// Confirm
let confirmButton = app.alerts.buttons["Delete"].firstMatch
if confirmButton.waitForExistence(timeout: defaultTimeout) {
confirmButton.tap()
}
// Verify deleted
_ = app.navigationBars.firstMatch.waitForExistence(timeout: defaultTimeout)
let deletedCard = app.staticTexts[deleteTitle]
XCTAssertTrue(deletedCard.waitForNonExistence(timeout: defaultTimeout), "Deleted warranty should not appear")
}
}
// MARK: - Edge Cases and Error Handling (warranty-side)
func test21_HandleEmptyWarrantiesList() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Search for non-existent warranty
searchFor(text: "NONEXISTENT_WARRANTY_99999")
let emptyState = app.otherElements[AccessibilityIdentifiers.Document.emptyStateView]
_ = emptyState.waitForExistence(timeout: defaultTimeout)
let hasNoItems = app.cells.count == 0
XCTAssertTrue(emptyState.exists || hasNoItems, "Should handle empty warranties list gracefully")
clearSearch()
}
func test23_CreateWarrantyWithSpecialCharacters() {
prepareDocumentsScreen()
switchToWarrantiesTab()
openDocumentForm()
let specialTitle = "Warranty w/ Special #Chars: @ & $ % \(UUID().uuidString.prefix(8))"
selectProperty()
docForm.titleField.focusAndType(specialTitle, app: app)
fillTextField(identifier: AccessibilityIdentifiers.Document.itemNameField, text: "Test @#$ Item") // REQUIRED
fillTextField(identifier: AccessibilityIdentifiers.Document.providerField, text: "Special & Co.") // REQUIRED
submitForm()
// Track via API (also gives server time to process)
trackDocumentForCleanup(title: specialTitle)
// Re-navigate to refresh the list after creation
navigateToDocuments()
switchToWarrantiesTab()
// Verify it was created (partial match with wait)
let partialTitle = String(specialTitle.prefix(20))
let warrantyCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(partialTitle)'")).firstMatch
XCTAssertTrue(warrantyCard.waitForExistence(timeout: loginTimeout), "Warranty with special characters should be created")
}
func test25_MultipleFiltersCombined() {
prepareDocumentsScreen()
switchToWarrantiesTab()
// Apply multiple filters
toggleActiveFilter() // Turn off active filter
let filterApplied = applyFilter(filterName: "Appliances")
searchFor(text: "Test")
// Should apply all filters without crashing.
// The search field may not persist in all UI states, so only
// assert it when it's expected to be present. The real goal of
// this test is verifying that combining filters doesn't crash.
let searchField = app.searchFields.firstMatch
if searchField.exists {
XCTAssertTrue(true, "Search field still present after combining filters")
}
// No hard assertion reaching this point without a crash is success.
// Clean up
clearSearch()
if filterApplied {
applyFilter(filterName: "All Categories")
}
toggleActiveFilter() // Turn active filter back on
}
}