Rearchitect UI test suite for complete, non-flaky coverage against live API

- Migrate Suite4-10, SmokeTests, NavigationCriticalPathTests to AuthenticatedTestCase
  with seeded admin account and real backend login
- Add 34 accessibility identifiers across 11 app views (task completion, profile,
  notifications, theme, join residence, manage users, forms)
- Create FeatureCoverageTests (14 tests) covering previously untested features:
  profile edit, theme selection, notification prefs, task completion, manage users,
  join residence, task templates
- Create MultiUserSharingTests (18 API tests) and MultiUserSharingUITests (8 XCUI
  tests) for full cross-user residence sharing lifecycle
- Add cleanup infrastructure: SuiteZZ_CleanupTests auto-wipes test data after runs,
  cleanup_test_data.sh script for manual reset via admin API
- Add share code API methods to TestAccountAPIClient (generateShareCode, joinWithCode,
  getShareCode, listResidenceUsers, removeUser)
- Fix app bugs found by tests:
  - ResidencesListView join callback now uses forceRefresh:true
  - APILayer invalidates task cache when residence count changes
  - AllTasksView auto-reloads tasks when residence list changes
- Fix test quality: keyboard focus waits, Save/Add button label matching,
  Documents tab label (Docs), remove API verification from UI tests
- DataLayerTests and PasswordResetTests now verify through UI, not API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
treyt
2026-03-15 17:32:13 -05:00
parent cf2e6d8bcc
commit 5c360a2796
57 changed files with 3781 additions and 928 deletions

View File

@@ -104,6 +104,7 @@ struct TaskFormView: View {
Text(residence.name).tag(residence as ResidenceResponse?)
}
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.residencePicker)
if !residenceError.isEmpty {
FieldError(message: residenceError)
@@ -163,6 +164,7 @@ struct TaskFormView: View {
VStack(alignment: .leading, spacing: 8) {
TextField(L10n.Tasks.titleLabel, text: $title)
.focused($focusedField, equals: .title)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.titleField)
.onChange(of: title) { _, newValue in
updateSuggestions(query: newValue)
}
@@ -185,6 +187,7 @@ struct TaskFormView: View {
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
.lineLimit(3...6)
.focused($focusedField, equals: .description)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.descriptionField)
} header: {
Text(L10n.Tasks.taskDetails)
} footer: {
@@ -201,6 +204,7 @@ struct TaskFormView: View {
Text(category.name.capitalized).tag(category as TaskCategory?)
}
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.categoryPicker)
} header: {
Text(L10n.Tasks.category)
}
@@ -213,6 +217,7 @@ struct TaskFormView: View {
Text(frequency.displayName).tag(frequency as TaskFrequency?)
}
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.frequencyPicker)
.onChange(of: selectedFrequency) { _, newFrequency in
// Clear interval days if not Custom frequency
if newFrequency?.name.lowercased() != "custom" {
@@ -225,9 +230,11 @@ struct TaskFormView: View {
TextField(L10n.Tasks.customInterval, text: $intervalDays)
.keyboardType(.numberPad)
.focused($focusedField, equals: .intervalDays)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.intervalDaysField)
}
DatePicker(L10n.Tasks.dueDate, selection: $dueDate, displayedComponents: .date)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.dueDatePicker)
} header: {
Text(L10n.Tasks.scheduling)
} footer: {
@@ -246,6 +253,7 @@ struct TaskFormView: View {
Text(priority.displayName).tag(priority as TaskPriority?)
}
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.priorityPicker)
Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress)
} header: {
@@ -257,6 +265,7 @@ struct TaskFormView: View {
TextField(L10n.Tasks.estimatedCost, text: $estimatedCost)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .estimatedCost)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.estimatedCostField)
}
.sectionBackground()
@@ -279,6 +288,7 @@ struct TaskFormView: View {
isPresented = false
}
.disabled(isLoadingLookups)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.formCancelButton)
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -286,6 +296,7 @@ struct TaskFormView: View {
submitForm()
}
.disabled(!canSave || viewModel.isLoading || isLoadingLookups)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.saveButton)
}
ToolbarItemGroup(placement: .keyboard) {