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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user