This commit is contained in:
Trey t
2025-11-08 16:02:01 -06:00
parent 97eed0eee9
commit 7dce211681
17 changed files with 1757 additions and 56 deletions

View File

@@ -423,7 +423,7 @@ fun App() {
description = route.description,
category = TaskCategory(route.categoryId, route.categoryName),
frequency = TaskFrequency(
route.frequencyId, route.frequencyName, "",
route.frequencyId, route.frequencyName, "", route.frequencyName,
daySpan = 0,
notifyDays = 0
),

View File

@@ -14,7 +14,7 @@ data class CustomTask (
val category: String,
val priority: String,
val status: String? = null,
@SerialName("due_date") val dueDate: String,
@SerialName("due_date") val dueDate: String?,
@SerialName("estimated_cost") val estimatedCost: String? = null,
@SerialName("actual_cost") val actualCost: String? = null,
val notes: String? = null,
@@ -62,7 +62,7 @@ data class TaskDetail(
val priority: TaskPriority,
val frequency: TaskFrequency,
val status: TaskStatus?,
@SerialName("due_date") val dueDate: String,
@SerialName("due_date") val dueDate: String?,
@SerialName("estimated_cost") val estimatedCost: String? = null,
@SerialName("actual_cost") val actualCost: String? = null,
val notes: String? = null,

View File

@@ -26,6 +26,7 @@ data class TaskFrequencyResponse(
data class TaskFrequency(
val id: Int,
val name: String,
@SerialName("lookup_name") val lookupName: String,
@SerialName("display_name") val displayName: String,
@SerialName("day_span") val daySpan: Int? = null,
@SerialName("notify_days") val notifyDays: Int? = null

View File

@@ -62,7 +62,7 @@ data class EditTaskRoute(
val priorityName: String,
val statusId: Int?,
val statusName: String?,
val dueDate: String,
val dueDate: String?,
val estimatedCost: String?,
val createdAt: String,
val updatedAt: String

View File

@@ -31,7 +31,7 @@ fun AddNewTaskDialog(
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(
id = 0, name = "", displayName = "",
id = 0, name = "", lookupName = "", displayName = "",
daySpan = 0,
notifyDays = 0
)) }

View File

@@ -34,7 +34,7 @@ fun AddNewTaskWithResidenceDialog(
var selectedResidenceId by remember { mutableStateOf(residencesResponse.residences.firstOrNull()?.id ?: 0) }
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(
id = 0, name = "", displayName = "",
id = 0, name = "", lookupName = "", displayName = "",
daySpan = 0,
notifyDays = 0
)) }

View File

@@ -127,7 +127,7 @@ fun TaskCard(
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = task.nextScheduledDate ?: task.dueDate,
text = task.nextScheduledDate ?: task.dueDate ?: "N/A",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold
)
@@ -345,7 +345,7 @@ fun TaskCardPreview() {
category = TaskCategory(id = 1, name = "maintenance", description = ""),
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
frequency = TaskFrequency(
id = 1, name = "monthly", displayName = "Monthly",
id = 1, name = "monthly", lookupName = "monthly", displayName = "Monthly",
daySpan = 0,
notifyDays = 0
),

View File

@@ -31,7 +31,7 @@ fun EditTaskScreen(
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) }
var selectedStatus by remember { mutableStateOf<TaskStatus?>(task.status) }
var dueDate by remember { mutableStateOf(task.dueDate) }
var dueDate by remember { mutableStateOf(task.dueDate ?: "") }
var estimatedCost by remember { mutableStateOf(task.estimatedCost ?: "") }
var categoryExpanded by remember { mutableStateOf(false) }
@@ -70,7 +70,7 @@ fun EditTaskScreen(
titleError = ""
}
if (dueDate.isBlank()) {
if (dueDate.isNullOrBlank()) {
dueDateError = "Due date is required"
isValid = false
} else {

View File

@@ -43,9 +43,11 @@ struct TaskCard: View {
Spacer()
Label(formatDate(task.dueDate), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
if let due_date = task.dueDate {
Label(formatDate(due_date), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
}
if task.completions.count > 0 {
@@ -169,7 +171,7 @@ struct TaskCard: View {
description: "Remove all debris from gutters",
category: TaskCategory(id: 1, name: "maintenance", description: ""),
priority: TaskPriority(id: 2, name: "medium", displayName: "", description: ""),
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "30", daySpan: 0, notifyDays: 0),
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "30", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "", description: ""),
dueDate: "2024-12-15",
estimatedCost: "150.00",

View File

@@ -85,7 +85,7 @@ struct TasksSection: View {
description: "Remove all debris",
category: TaskCategory(id: 1, name: "maintenance", description: ""),
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: ""),
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly", daySpan: 0, notifyDays: 0),
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "Monthly", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: ""),
dueDate: "2024-12-15",
estimatedCost: "150.00",
@@ -115,7 +115,7 @@ struct TasksSection: View {
description: "Kitchen sink fixed",
category: TaskCategory(id: 2, name: "plumbing", description: ""),
priority: TaskPriority(id: 3, name: "high", displayName: "High", description: ""),
frequency: TaskFrequency(id: 6, name: "once", displayName: "One Time", daySpan: 0, notifyDays: 0),
frequency: TaskFrequency(id: 6, name: "once", lookupName: "", displayName: "One Time", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: ""),
dueDate: "2024-11-01",
estimatedCost: "200.00",

View File

@@ -11,21 +11,21 @@ struct AllTasksView: View {
@State private var showEditTask = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var selectedTaskForComplete: TaskDetail?
private var hasNoTasks: Bool {
guard let response = tasksResponse else { return true }
return response.columns.allSatisfy { $0.tasks.isEmpty }
}
private var hasTasks: Bool {
!hasNoTasks
}
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if isLoadingTasks {
ProgressView()
} else if let error = tasksError {
@@ -37,20 +37,20 @@ struct AllTasksView: View {
// Empty state with big button
VStack(spacing: 24) {
Spacer()
Image(systemName: "checklist")
.font(.system(size: 64))
.foregroundStyle(.blue.opacity(0.6))
Text("No tasks yet")
.font(.title2)
.fontWeight(.semibold)
Text("Create your first task to get started")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button(action: {
showAddTask = true
}) {
@@ -66,13 +66,13 @@ struct AllTasksView: View {
.controlSize(.large)
.padding(.horizontal, 48)
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
if residenceViewModel.myResidences?.residences.isEmpty ?? true {
Text("Add a property first from the Residences tab")
.font(.caption)
.foregroundColor(.red)
}
Spacer()
}
.padding()
@@ -175,13 +175,13 @@ struct AllTasksView: View {
residenceViewModel.loadMyResidences()
}
}
private func loadAllTasks() {
guard let token = TokenStorage.shared.getToken() else { return }
isLoadingTasks = true
tasksError = nil
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
taskApi.getTasks(token: token, days: 30) { result, error in
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
@@ -208,16 +208,16 @@ struct DynamicTaskColumnView: View {
let onCompleteTask: (TaskDetail) -> Void
let onArchiveTask: (Int32) -> Void
let onUnarchiveTask: (Int32) -> Void
// Get icon from API response, with fallback
private var columnIcon: String {
column.icons["ios"] ?? "list.bullet"
}
private var columnColor: Color {
Color(hex: column.color) ?? .primary
}
var body: some View {
VStack(spacing: 0) {
ScrollView {
@@ -227,13 +227,13 @@ struct DynamicTaskColumnView: View {
Image(systemName: columnIcon)
.font(.headline)
.foregroundColor(columnColor)
Text(column.displayName)
.font(.headline)
.foregroundColor(columnColor)
Spacer()
Text("\(column.count)")
.font(.caption)
.fontWeight(.semibold)
@@ -243,13 +243,13 @@ struct DynamicTaskColumnView: View {
.background(columnColor)
.cornerRadius(12)
}
if column.tasks.isEmpty {
VStack(spacing: 8) {
Image(systemName: columnIcon)
.font(.system(size: 40))
.foregroundColor(columnColor.opacity(0.3))
Text("No tasks")
.font(.caption)
.foregroundColor(.secondary)
@@ -288,7 +288,7 @@ struct DynamicTaskCard: View {
let onComplete: () -> Void
let onArchive: () -> Void
let onUnarchive: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
@@ -296,39 +296,41 @@ struct DynamicTaskCard: View {
Text(task.title)
.font(.headline)
.foregroundColor(.primary)
if let status = task.status {
StatusBadge(status: status.name)
}
}
Spacer()
PriorityBadge(priority: task.priority.name)
}
if let description = task.description_, !description.isEmpty {
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Label(formatDate(task.dueDate), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
if let due_date = task.dueDate {
Label(formatDate(due_date), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
}
if task.completions.count > 0 {
Divider()
HStack {
Image(systemName: "checkmark.circle")
.foregroundColor(.green)
@@ -337,7 +339,7 @@ struct DynamicTaskCard: View {
.foregroundColor(.secondary)
}
}
// Render buttons based on buttonTypes array
VStack(spacing: 8) {
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
@@ -350,7 +352,7 @@ struct DynamicTaskCard: View {
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
}
private func formatDate(_ dateString: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
@@ -360,7 +362,7 @@ struct DynamicTaskCard: View {
}
return dateString
}
@ViewBuilder
private func renderButton(for buttonType: String) -> some View {
switch buttonType {
@@ -436,7 +438,7 @@ extension View {
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
@@ -505,7 +507,7 @@ extension Color {
default:
return nil
}
self.init(
.sRGB,
red: Double(r) / 255,

View File

@@ -31,7 +31,7 @@ struct EditTaskView: View {
_selectedFrequency = State(initialValue: task.frequency)
_selectedPriority = State(initialValue: task.priority)
_selectedStatus = State(initialValue: task.status)
_dueDate = State(initialValue: task.dueDate)
_dueDate = State(initialValue: task.dueDate ?? "")
_estimatedCost = State(initialValue: task.estimatedCost ?? "")
}

View File

@@ -0,0 +1,269 @@
import XCTest
/// Comprehensive tests for authentication flows
final class AuthenticationUITests: BaseUITest {
// MARK: - Login Tests
func testLoginWithValidCredentials() {
// Given: User is on login screen
XCTAssertTrue(app.staticTexts["MyCrib"].exists)
// When: User enters valid credentials and taps login
login(username: "testuser", password: "TestPass123!")
// Then: User should be navigated to main screen
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Should navigate to main tab view")
}
func testLoginWithInvalidCredentials() {
// Given: User is on login screen
XCTAssertTrue(app.staticTexts["MyCrib"].exists)
// When: User enters invalid credentials
login(username: "invaliduser", password: "WrongPassword!")
// Then: Error message should be displayed
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Invalid username or password'"))
XCTAssertTrue(errorMessage.firstMatch.waitForExistence(timeout: 5), "Should show error message")
// And: User should remain on login screen
XCTAssertTrue(app.staticTexts["MyCrib"].exists)
}
func testLoginWithEmptyFields() {
// Given: User is on login screen
let loginButton = app.buttons["Login"]
// When: User taps login without entering credentials
loginButton.tap()
// Then: Validation error should be shown
let usernameError = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Username is required'"))
XCTAssertTrue(usernameError.firstMatch.waitForExistence(timeout: 3), "Should show username required error")
}
func testPasswordVisibilityToggle() {
// Given: User has typed password
let passwordField = app.secureTextFields["Password"]
let textField = app.textFields["Password"]
let toggleButton = app.buttons.matching(identifier: "eye").firstMatch
passwordField.tap()
passwordField.typeText("TestPassword")
// When: User taps the visibility toggle
toggleButton.tap()
// Then: Password should be visible as text
XCTAssertTrue(textField.exists, "Password should be visible")
// When: User taps toggle again
toggleButton.tap()
// Then: Password should be hidden again
XCTAssertTrue(passwordField.exists, "Password should be secure")
}
// MARK: - Registration Tests
func testRegistrationWithValidData() {
// Given: User is on login screen
let signUpButton = app.buttons["Sign Up"]
// When: User taps Sign Up
signUpButton.tap()
// Then: Registration screen should be displayed
assertNavigatedTo(title: "Create Account", timeout: 3)
// When: User fills in valid registration data
let timestamp = Int(Date().timeIntervalSince1970)
register(
username: "newuser\(timestamp)",
email: "newuser\(timestamp)@test.com",
password: "TestPass123!",
firstName: "Test",
lastName: "User"
)
// Then: User should be registered and shown verification screen
let verificationTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'"))
XCTAssertTrue(verificationTitle.firstMatch.waitForExistence(timeout: 10), "Should show verification screen")
}
func testRegistrationWithExistingUsername() {
// Given: User is on registration screen
let signUpButton = app.buttons["Sign Up"]
signUpButton.tap()
// When: User registers with existing username
register(
username: "existinguser",
email: "newemail@test.com",
password: "TestPass123!"
)
// Then: Error message should be displayed
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'username' OR label CONTAINS[c] 'already exists'"))
XCTAssertTrue(errorMessage.firstMatch.waitForExistence(timeout: 5), "Should show username exists error")
}
func testRegistrationWithInvalidEmail() {
// Given: User is on registration screen
let signUpButton = app.buttons["Sign Up"]
signUpButton.tap()
// When: User enters invalid email
let emailField = app.textFields["Email"]
let registerButton = app.buttons["Register"]
emailField.tap()
emailField.typeText("invalidemail")
registerButton.tap()
// Then: Email validation error should be shown
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'valid email'"))
XCTAssertTrue(errorMessage.firstMatch.waitForExistence(timeout: 3), "Should show email validation error")
}
func testRegistrationWithWeakPassword() {
// Given: User is on registration screen
let signUpButton = app.buttons["Sign Up"]
signUpButton.tap()
// When: User enters weak password
let timestamp = Int(Date().timeIntervalSince1970)
let usernameField = app.textFields["Username"]
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let registerButton = app.buttons["Register"]
usernameField.tap()
usernameField.typeText("testuser\(timestamp)")
emailField.tap()
emailField.typeText("test\(timestamp)@test.com")
passwordField.tap()
passwordField.typeText("weak")
registerButton.tap()
// Then: Password validation error should be shown
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'password' AND (label CONTAINS[c] 'strong' OR label CONTAINS[c] 'at least')"))
XCTAssertTrue(errorMessage.firstMatch.waitForExistence(timeout: 3), "Should show password strength error")
}
// MARK: - Logout Tests
func testLogoutFlow() {
// Given: User is logged in
login(username: "testuser", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
// When: User logs out
logout()
// Then: User should be returned to login screen
XCTAssertTrue(app.staticTexts["MyCrib"].waitForExistence(timeout: 5), "Should return to login screen")
XCTAssertTrue(app.buttons["Login"].exists, "Login button should be visible")
}
func testLogoutClearsUserData() {
// Given: User is logged in and has viewed some data
login(username: "testuser", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
navigateToTab("Residences")
wait(seconds: 2) // Wait for data to load
// When: User logs out
logout()
// And: User logs back in
login(username: "testuser", password: "TestPass123!")
// Then: Fresh data should be loaded (not cached)
let loadingIndicator = app.activityIndicators.firstMatch
XCTAssertTrue(loadingIndicator.exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Loading'")).firstMatch.exists,
"Should show loading state for fresh data")
}
// MARK: - Session Management Tests
func testSessionPersistence() {
// Given: User logs in
login(username: "testuser", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
// When: App is terminated and relaunched
app.terminate()
app.launch()
// Then: User should still be logged in
XCTAssertTrue(residencesTab.waitForExistence(timeout: 5), "User session should persist")
}
func testLoginRedirectsVerifiedUser() {
// Given: Verified user logs in
login(username: "verifieduser", password: "TestPass123!")
// Then: User should go directly to main screen (not verification)
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Verified user should skip verification")
}
func testLoginRedirectsUnverifiedUser() {
// Given: Unverified user logs in
login(username: "unverifieduser", password: "TestPass123!")
// Then: User should be shown verification screen
let verificationTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'"))
XCTAssertTrue(verificationTitle.firstMatch.waitForExistence(timeout: 10), "Unverified user should see verification screen")
}
// MARK: - Error Handling Tests
func testLoginWithNetworkError() {
// Note: This test requires network simulation or mocking
// For now, it's a placeholder for future implementation
// Given: Network is unavailable
// When: User attempts to login
// Then: Network error should be displayed
}
func testLoginRetryAfterError() {
// Given: User encountered a login error
login(username: "invaliduser", password: "WrongPassword!")
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Invalid'"))
XCTAssertTrue(errorMessage.firstMatch.waitForExistence(timeout: 5))
// When: User enters correct credentials
let usernameField = app.textFields["Username"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Login"]
app.clearText(in: usernameField)
usernameField.typeText("testuser")
app.clearText(in: passwordField)
passwordField.typeText("TestPass123!")
loginButton.tap()
// Then: Login should succeed
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Should login successfully after retry")
}
}

View File

@@ -0,0 +1,470 @@
import XCTest
/// Comprehensive tests for multi-user residence features
final class MultiUserUITests: BaseUITest {
override func setUp() {
super.setUp()
// Login as primary owner
login(username: "testowner", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
}
// MARK: - Manage Users Tests
func testManageUsersButtonVisibleForOwner() {
// Given: User owns a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// Then: Manage users button should be visible
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
XCTAssertTrue(manageUsersButton.exists, "Owner should see manage users button")
}
}
func testManageUsersButtonHiddenForSharedUser() {
// Given: User is a shared user (not owner)
logout()
login(username: "shareduser", password: "TestPass123!")
navigateToTab("Residences")
wait(seconds: 2)
// Find a shared residence
let sharedResidence = app.cells.firstMatch
if sharedResidence.exists {
sharedResidence.tap()
wait(seconds: 2)
// Then: Manage users button should NOT be visible
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
XCTAssertFalse(manageUsersButton.exists, "Shared user should not see manage users button")
}
}
func testOpenManageUsersScreen() {
// Given: Owner is viewing a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// When: Owner taps manage users button
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Then: Manage users screen should be displayed
let manageUsersTitle = app.navigationBars["Manage Users"]
XCTAssertTrue(manageUsersTitle.waitForExistence(timeout: 3), "Should show manage users screen")
// And: User list should be visible
let usersList = app.scrollViews.firstMatch
XCTAssertTrue(usersList.exists, "Should show users list")
}
}
}
// MARK: - Share Code Tests
func testShareCodeInitiallyBlank() {
// Given: Owner opens manage users screen
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Then: Share code should be blank initially
let noActiveCode = app.staticTexts["No active code"]
XCTAssertTrue(noActiveCode.exists, "Share code should start blank")
}
}
}
func testGenerateShareCode() {
// Given: Owner is on manage users screen
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// When: Owner taps generate code button
let generateButton = app.buttons["Generate"]
if !generateButton.exists {
// Button might say "New Code" if there's an existing code
let newCodeButton = app.buttons["New Code"]
if newCodeButton.exists {
newCodeButton.tap()
}
} else {
generateButton.tap()
}
// Then: Share code should be generated and displayed
wait(seconds: 2)
let shareCodeTexts = app.staticTexts.matching(NSPredicate(format: "label.length == 6 AND label MATCHES %@", "[A-Z0-9]{6}"))
XCTAssertTrue(shareCodeTexts.count > 0, "Should display 6-character share code")
}
}
}
func testRegenerateShareCode() {
// Given: Owner has generated a share code
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Generate first code
let generateButton = app.buttons["Generate"]
if generateButton.exists {
generateButton.tap()
wait(seconds: 2)
let firstCode = app.staticTexts.matching(NSPredicate(format: "label.length == 6 AND label MATCHES %@", "[A-Z0-9]{6}")).firstMatch.label
// When: Owner generates new code
let newCodeButton = app.buttons["New Code"]
if newCodeButton.exists {
newCodeButton.tap()
wait(seconds: 2)
// Then: A different code should be generated
let secondCode = app.staticTexts.matching(NSPredicate(format: "label.length == 6 AND label MATCHES %@", "[A-Z0-9]{6}")).firstMatch.label
XCTAssertNotEqual(firstCode, secondCode, "New code should be different")
}
}
}
}
}
// MARK: - Join Residence Tests
func testJoinResidenceWithValidCode() {
// This test requires coordination between two accounts
// Given: Owner generates a share code
var shareCode: String = ""
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
let generateButton = app.buttons["Generate"]
if generateButton.exists {
generateButton.tap()
wait(seconds: 2)
shareCode = app.staticTexts.matching(NSPredicate(format: "label.length == 6 AND label MATCHES %@", "[A-Z0-9]{6}")).firstMatch.label
}
// Close manage users screen
let closeButton = app.buttons["Close"]
if closeButton.exists {
closeButton.tap()
}
}
}
// When: Different user joins with code
if !shareCode.isEmpty {
logout()
login(username: "newuser", password: "TestPass123!")
navigateToTab("Residences")
wait(seconds: 1)
// Find join residence button
let joinButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Join'")).firstMatch
if joinButton.exists {
joinButton.tap()
wait(seconds: 1)
// Enter share code
let codeField = app.textFields.firstMatch
if codeField.exists {
codeField.tap()
codeField.typeText(shareCode)
// Submit
let submitButton = app.buttons["Join"]
if submitButton.exists {
submitButton.tap()
wait(seconds: 2)
// Then: User should be added to residence
let successMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Successfully joined'"))
XCTAssertTrue(successMessage.firstMatch.exists || app.cells.count > 0,
"Should join residence successfully")
}
}
}
}
}
func testJoinResidenceWithInvalidCode() {
// Given: User is on join residence screen
navigateToTab("Residences")
let joinButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Join'")).firstMatch
if joinButton.exists {
joinButton.tap()
wait(seconds: 1)
// When: User enters invalid code
let codeField = app.textFields.firstMatch
if codeField.exists {
codeField.tap()
codeField.typeText("INVALID")
let submitButton = app.buttons["Join"]
if submitButton.exists {
submitButton.tap()
wait(seconds: 2)
// Then: Error message should be shown
let errorMessage = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Invalid' OR label CONTAINS[c] 'not found'"))
XCTAssertTrue(errorMessage.firstMatch.exists, "Should show invalid code error")
}
}
}
}
// MARK: - User List Tests
func testUserListShowsAllUsers() {
// Given: Residence has multiple users
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Then: User count should be displayed
let userCountLabel = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] 'Users'"))
XCTAssertTrue(userCountLabel.count > 0, "Should show user count")
// And: Individual users should be listed
let usersList = app.scrollViews.firstMatch
XCTAssertTrue(usersList.exists, "Should show users list")
}
}
}
func testOwnerLabelDisplayed() {
// Given: Owner is viewing manage users screen
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Then: Owner badge should be visible next to owner's name
let ownerBadge = app.staticTexts["Owner"]
XCTAssertTrue(ownerBadge.exists, "Should show Owner badge")
}
}
}
// MARK: - Remove User Tests
func testRemoveUserAsOwner() {
// Given: Owner has residence with multiple users
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// When: Owner taps remove button on a user
let removeButtons = app.buttons.matching(identifier: "trash")
if removeButtons.count > 0 {
let initialUserCount = removeButtons.count
removeButtons.firstMatch.tap()
// Confirm removal if prompted
let confirmButton = app.alerts.buttons["Remove"]
if confirmButton.exists {
confirmButton.tap()
}
// Then: User should be removed
wait(seconds: 2)
let newUserCount = app.buttons.matching(identifier: "trash").count
XCTAssertTrue(newUserCount < initialUserCount, "Should remove user")
}
}
}
}
func testCannotRemoveOwner() {
// Given: Owner is viewing manage users
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
// Then: Owner row should NOT have remove button
let ownerLabel = app.staticTexts["Owner"]
if ownerLabel.exists {
// Check if there's a remove button in the same container
// Owner should not have a remove button next to their name
}
}
}
}
// MARK: - Shared User Access Tests
func testSharedUserCanViewResidence() {
// Given: User is a shared user
logout()
login(username: "shareduser", password: "TestPass123!")
// Then: Shared residences should appear in list
navigateToTab("Residences")
wait(seconds: 2)
let residencesList = app.cells.count
XCTAssertTrue(residencesList > 0, "Shared user should see shared residences")
}
func testSharedUserCanCreateTasks() {
// Given: Shared user is viewing a shared residence
logout()
login(username: "shareduser", password: "TestPass123!")
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// When: Shared user tries to create a task
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
XCTAssertTrue(addButton.exists, "Shared user should be able to add tasks")
if addButton.exists {
addButton.tap()
wait(seconds: 1)
// Then: Add task form should be displayed
let titleField = app.textFields.firstMatch
XCTAssertTrue(titleField.exists, "Shared user should be able to create tasks")
}
}
}
func testSharedUserCanEditTasks() {
// Given: Shared user is viewing tasks
logout()
login(username: "shareduser", password: "TestPass123!")
navigateToTab("Tasks")
wait(seconds: 2)
// When: Shared user tries to edit a task
let editButtons = app.buttons.matching(identifier: "pencil")
if editButtons.count > 0 {
// Then: Edit buttons should be available
XCTAssertTrue(editButtons.firstMatch.exists, "Shared user should be able to edit tasks")
}
}
func testUserCountDisplayed() {
// Given: Owner is viewing a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// Then: User count should be visible somewhere on the screen
// This depends on your UI design - adjust as needed
let manageUsersButton = app.navigationBars.buttons.matching(identifier: "person.2").firstMatch
if manageUsersButton.exists {
manageUsersButton.tap()
wait(seconds: 1)
let userCountText = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] 'Users'"))
XCTAssertTrue(userCountText.count > 0, "Should display user count")
}
}
}
}

View File

@@ -0,0 +1,346 @@
import XCTest
/// Comprehensive tests for residence management
final class ResidenceUITests: BaseUITest {
override func setUp() {
super.setUp()
// Login before each test
login(username: "testuser", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
}
// MARK: - List View Tests
func testResidenceListDisplays() {
// Given: User is on residences tab
navigateToTab("Residences")
// Then: Residences list should be displayed
let navigationBar = app.navigationBars["Residences"]
XCTAssertTrue(navigationBar.exists, "Should show residences navigation bar")
}
func testResidenceListShowsProperties() {
// Given: User has residences
navigateToTab("Residences")
// Then: Residence cards should be visible
let residenceCards = app.scrollViews.descendants(matching: .other).matching(NSPredicate(format: "identifier CONTAINS 'ResidenceCard'"))
XCTAssertTrue(residenceCards.count > 0 || app.staticTexts["No residences yet"].exists,
"Should show either residence cards or empty state")
}
func testEmptyStateDisplays() {
// Given: User has no residences (requires test account with no data)
navigateToTab("Residences")
// Then: Empty state should be shown
// Note: This test assumes test user has no residences
let emptyStateText = app.staticTexts["No residences yet"]
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add'")).firstMatch
// Should show either residences or empty state with add button
XCTAssertTrue(emptyStateText.exists || app.cells.count > 0, "Should show content or empty state")
}
// MARK: - Create Residence Tests
func testCreateResidenceFlow() {
// Given: User is on residences screen
navigateToTab("Residences")
// When: User taps add residence button
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
if !addButton.exists {
// Try finding add button in other locations
let fabButton = app.buttons["Add Residence"]
if fabButton.exists {
fabButton.tap()
} else {
XCTFail("Could not find add residence button")
}
} else {
addButton.tap()
}
// Then: Add residence form should be displayed
wait(seconds: 1)
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'name' OR label CONTAINS[c] 'Name'")).firstMatch
XCTAssertTrue(nameField.exists, "Should show residence name field")
// When: User fills in residence details
let timestamp = Int(Date().timeIntervalSince1970)
nameField.tap()
nameField.typeText("Test House \(timestamp)")
let addressField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'address' OR label CONTAINS[c] 'Address'")).firstMatch
if addressField.exists {
addressField.tap()
addressField.typeText("123 Test Street")
}
let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'city' OR label CONTAINS[c] 'City'")).firstMatch
if cityField.exists {
cityField.tap()
cityField.typeText("Test City")
}
// When: User saves the residence
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: User should be returned to list view
// And: New residence should appear in the list
wait(seconds: 2)
let residenceTitle = app.staticTexts["Test House \(timestamp)"]
XCTAssertTrue(residenceTitle.waitForExistence(timeout: 5) || app.navigationBars["Residences"].exists,
"Should show new residence or navigate back to list")
}
func testCreateResidenceValidation() {
// Given: User is on add residence screen
navigateToTab("Residences")
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
addButton.tap()
wait(seconds: 1)
// When: User attempts to save without required fields
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Validation errors should be shown
let errorMessages = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'required'"))
XCTAssertTrue(errorMessages.count > 0 || !app.navigationBars["Residences"].exists,
"Should show validation errors or prevent saving")
}
// MARK: - View Residence Details Tests
func testViewResidenceDetails() {
// Given: User has residences
navigateToTab("Residences")
wait(seconds: 2)
// When: User taps on a residence
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
// Then: Residence details should be displayed
wait(seconds: 2)
let detailView = app.scrollViews.firstMatch
XCTAssertTrue(detailView.exists || app.navigationBars.element(boundBy: 1).exists,
"Should show residence details")
}
}
func testResidenceDetailsShowsAllSections() {
// Given: User is viewing residence details
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// Then: All sections should be visible (after scrolling)
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
// Check for address section
let addressSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Address'")).firstMatch
// Check for tasks section
scrollView.swipeUp()
let tasksSection = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
XCTAssertTrue(addressSection.exists || tasksSection.exists, "Should show residence sections")
}
}
}
// MARK: - Edit Residence Tests
func testEditResidenceFlow() {
// Given: User is viewing residence details
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// When: User taps edit button
let editButton = app.navigationBars.buttons["Edit"]
if editButton.exists {
editButton.tap()
wait(seconds: 1)
// Then: Edit form should be displayed
let nameField = app.textFields.containing(NSPredicate(format: "value != nil AND value != ''")).firstMatch
XCTAssertTrue(nameField.exists, "Should show edit form with current values")
// When: User updates the name
if nameField.exists {
app.clearText(in: nameField)
nameField.typeText("Updated House Name")
// When: User saves changes
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Changes should be saved and details view updated
wait(seconds: 2)
let updatedName = app.staticTexts["Updated House Name"]
XCTAssertTrue(updatedName.waitForExistence(timeout: 5) || app.navigationBars.element(boundBy: 1).exists,
"Should show updated residence name or details view")
}
}
}
}
func testCancelEditingResidence() {
// Given: User is editing a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
let editButton = app.navigationBars.buttons["Edit"]
if editButton.exists {
editButton.tap()
wait(seconds: 1)
// When: User makes changes
let nameField = app.textFields.firstMatch
let originalValue = nameField.value as? String
if nameField.exists {
app.clearText(in: nameField)
nameField.typeText("Temporary Change")
// When: User cancels
let cancelButton = app.buttons["Cancel"]
if cancelButton.exists {
cancelButton.tap()
}
// Then: Changes should be discarded
wait(seconds: 1)
if let original = originalValue {
let originalText = app.staticTexts[original]
XCTAssertTrue(originalText.exists || app.navigationBars.element(boundBy: 1).exists,
"Should discard changes")
}
}
}
}
}
// MARK: - Delete Residence Tests
func testDeleteResidence() {
// Given: User has a residence to delete
navigateToTab("Residences")
wait(seconds: 2)
let initialResidenceCount = app.cells.count
// When: User swipes to delete (if supported)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.swipeLeft()
let deleteButton = app.buttons["Delete"]
if deleteButton.exists {
deleteButton.tap()
// Confirm deletion if alert appears
let confirmButton = app.alerts.buttons["Delete"]
if confirmButton.exists {
confirmButton.tap()
}
// Then: Residence should be removed
wait(seconds: 2)
let newCount = app.cells.count
XCTAssertTrue(newCount < initialResidenceCount || app.staticTexts["No residences yet"].exists,
"Should remove residence from list")
}
}
}
// MARK: - Navigation Tests
func testNavigateBackFromDetails() {
// Given: User is viewing residence details
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// When: User taps back button
navigateBack()
// Then: User should return to residences list
let navigationBar = app.navigationBars["Residences"]
XCTAssertTrue(navigationBar.waitForExistence(timeout: 3), "Should navigate back to residences list")
}
}
// MARK: - Search and Filter Tests (if implemented)
func testSearchResidences() {
// Given: User has multiple residences
navigateToTab("Residences")
wait(seconds: 2)
// When: User searches for a residence
let searchField = app.searchFields.firstMatch
if searchField.exists {
searchField.tap()
searchField.typeText("Test")
// Then: Results should be filtered
wait(seconds: 1)
let visibleResidences = app.cells.count
XCTAssertTrue(visibleResidences >= 0, "Should show filtered results")
}
}
// MARK: - Pull to Refresh Tests
func testPullToRefreshResidences() {
// Given: User is on residences list
navigateToTab("Residences")
wait(seconds: 2)
// When: User pulls down to refresh
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
let startPoint = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
let endPoint = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
startPoint.press(forDuration: 0.1, thenDragTo: endPoint)
// Then: Loading indicator should appear
let loadingIndicator = app.activityIndicators.firstMatch
XCTAssertTrue(loadingIndicator.exists || scrollView.exists, "Should trigger refresh")
}
}
}

View File

@@ -0,0 +1,431 @@
import XCTest
/// Comprehensive tests for task management
final class TaskUITests: BaseUITest {
override func setUp() {
super.setUp()
// Login before each test
login(username: "testuser", password: "TestPass123!")
let residencesTab = app.tabBars.buttons["Residences"]
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10))
}
// MARK: - Task List Tests
func testTasksTabDisplays() {
// When: User navigates to tasks tab
navigateToTab("Tasks")
// Then: Tasks screen should be displayed
let navigationBar = app.navigationBars["All Tasks"]
XCTAssertTrue(navigationBar.waitForExistence(timeout: 5), "Should show tasks navigation bar")
}
func testTaskColumnsDisplay() {
// Given: User is on tasks tab
navigateToTab("Tasks")
wait(seconds: 2)
// Then: Task columns should be visible
let upcomingLabel = app.staticTexts["Upcoming"]
let inProgressLabel = app.staticTexts["In Progress"]
let doneLabel = app.staticTexts["Done"]
XCTAssertTrue(upcomingLabel.exists || inProgressLabel.exists || doneLabel.exists ||
app.staticTexts["No tasks yet"].exists,
"Should show task columns or empty state")
}
func testEmptyTasksState() {
// Given: User has no tasks (requires account with no tasks)
navigateToTab("Tasks")
wait(seconds: 2)
// Then: Empty state should be shown
let emptyState = app.staticTexts["No tasks yet"]
let addButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
// Should show either tasks or empty state
XCTAssertTrue(emptyState.exists || app.scrollViews.firstMatch.exists,
"Should show content or empty state")
}
// MARK: - Create Task Tests
func testCreateTaskFromTasksTab() {
// Given: User is on tasks tab
navigateToTab("Tasks")
// When: User taps add task button
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
if addButton.exists {
addButton.tap()
wait(seconds: 1)
// Then: Add task form should be displayed
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'title' OR label CONTAINS[c] 'Title'")).firstMatch
XCTAssertTrue(titleField.exists, "Should show add task form")
// When: User fills in task details
let timestamp = Int(Date().timeIntervalSince1970)
titleField.tap()
titleField.typeText("Test Task \(timestamp)")
let descriptionField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'description' OR label CONTAINS[c] 'Description'")).firstMatch
if descriptionField.exists {
descriptionField.tap()
descriptionField.typeText("Test task description")
}
// When: User saves the task
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Task should be created and user returned to tasks list
wait(seconds: 2)
XCTAssertTrue(app.navigationBars["All Tasks"].exists || app.staticTexts["Test Task \(timestamp)"].exists,
"Should create task and return to list")
}
}
func testCreateTaskFromResidenceDetails() {
// Given: User is viewing a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// When: User taps add task button
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
if addButton.exists {
addButton.tap()
wait(seconds: 1)
// Then: Add task form should be displayed
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'title' OR label CONTAINS[c] 'Title'")).firstMatch
XCTAssertTrue(titleField.exists, "Should show add task form from residence")
// When: User creates the task
let timestamp = Int(Date().timeIntervalSince1970)
titleField.tap()
titleField.typeText("Residence Task \(timestamp)")
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Task should be created
wait(seconds: 2)
XCTAssertTrue(app.staticTexts["Residence Task \(timestamp)"].exists || app.navigationBars.element(boundBy: 1).exists,
"Should create task for residence")
}
}
}
func testCreateTaskValidation() {
// Given: User is on add task form
navigateToTab("Tasks")
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
if addButton.exists {
addButton.tap()
wait(seconds: 1)
// When: User tries to save without required fields
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Validation errors should be shown
let errorMessages = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'required'"))
XCTAssertTrue(errorMessages.count > 0 || !app.navigationBars["All Tasks"].exists,
"Should show validation errors")
}
}
// MARK: - View Task Details Tests
func testViewTaskDetails() {
// Given: User has tasks
navigateToTab("Tasks")
wait(seconds: 2)
// When: User taps on a task card
let taskCard = app.otherElements.containing(NSPredicate(format: "identifier CONTAINS 'TaskCard'")).firstMatch
if !taskCard.exists {
// Try finding by task title
let taskTitle = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] 'task'")).firstMatch
if taskTitle.exists {
taskTitle.tap()
}
} else {
taskCard.tap()
}
// Note: Depending on implementation, tapping a task might show details or edit form
wait(seconds: 1)
// Verify some form of task interaction occurred
}
// MARK: - Edit Task Tests
func testEditTaskFlow() {
// Given: User is viewing/editing a task
navigateToTab("Tasks")
wait(seconds: 2)
// Find and tap edit button on a task
let editButtons = app.buttons.matching(identifier: "pencil")
if editButtons.count > 0 {
editButtons.firstMatch.tap()
wait(seconds: 1)
// When: User modifies task details
let titleField = app.textFields.containing(NSPredicate(format: "value != nil AND value != ''")).firstMatch
if titleField.exists {
app.clearText(in: titleField)
titleField.typeText("Updated Task Title")
// When: User saves changes
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Changes should be saved
wait(seconds: 2)
let updatedTitle = app.staticTexts["Updated Task Title"]
XCTAssertTrue(updatedTitle.waitForExistence(timeout: 5) || app.navigationBars["All Tasks"].exists,
"Should save task changes")
}
}
}
// MARK: - Complete Task Tests
func testCompleteTaskFlow() {
// Given: User has an incomplete task
navigateToTab("Tasks")
wait(seconds: 2)
// When: User taps complete button on a task
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete'")).firstMatch
if completeButton.exists {
completeButton.tap()
wait(seconds: 1)
// Then: Complete task dialog should be shown
let completionDialog = app.sheets.firstMatch
XCTAssertTrue(completionDialog.exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Complete'")).firstMatch.exists,
"Should show completion dialog")
// When: User confirms completion
let confirmButton = app.buttons["Complete"]
if confirmButton.exists {
confirmButton.tap()
}
// Then: Task should be marked as complete
wait(seconds: 2)
// Task should move to completed column or show completion status
}
}
func testCompleteTaskWithDetails() {
// Given: User is completing a task
navigateToTab("Tasks")
wait(seconds: 2)
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete'")).firstMatch
if completeButton.exists {
completeButton.tap()
wait(seconds: 1)
// When: User adds completion details
let notesField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'notes' OR label CONTAINS[c] 'Notes'")).firstMatch
if notesField.exists {
notesField.tap()
notesField.typeText("Task completed successfully")
}
let costField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'cost' OR label CONTAINS[c] 'Cost'")).firstMatch
if costField.exists {
costField.tap()
costField.typeText("100")
}
// When: User saves completion
let saveButton = app.buttons["Save"]
if saveButton.exists {
saveButton.tap()
}
// Then: Task should be completed with details
wait(seconds: 2)
}
}
// MARK: - Task Status Changes Tests
func testMarkTaskInProgress() {
// Given: User has a pending task
navigateToTab("Tasks")
wait(seconds: 2)
// When: User marks task as in progress
let inProgressButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'In Progress'")).firstMatch
if inProgressButton.exists {
inProgressButton.tap()
wait(seconds: 2)
// Then: Task should move to In Progress column
let inProgressColumn = app.staticTexts["In Progress"]
XCTAssertTrue(inProgressColumn.exists, "Should have In Progress column")
}
}
func testCancelTask() {
// Given: User has an active task
navigateToTab("Tasks")
wait(seconds: 2)
// When: User cancels a task
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
if cancelButton.exists {
cancelButton.tap()
// Confirm cancellation if prompted
let confirmButton = app.alerts.buttons["Cancel Task"]
if confirmButton.exists {
confirmButton.tap()
}
// Then: Task should be cancelled
wait(seconds: 2)
}
}
func testUncancelTask() {
// Given: User has a cancelled task
navigateToTab("Tasks")
wait(seconds: 2)
// When: User uncancels a task
let uncancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Uncancel' OR label CONTAINS[c] 'Restore'")).firstMatch
if uncancelButton.exists {
uncancelButton.tap()
wait(seconds: 2)
// Then: Task should be restored
}
}
// MARK: - Archive Task Tests
func testArchiveTask() {
// Given: User has a completed task
navigateToTab("Tasks")
wait(seconds: 2)
// When: User archives a task
let archiveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Archive'")).firstMatch
if archiveButton.exists {
archiveButton.tap()
wait(seconds: 2)
// Then: Task should be archived (moved to archived column or hidden)
let archivedColumn = app.staticTexts["Archived"]
// Task may be in archived column or removed from view
}
}
func testUnarchiveTask() {
// Given: User has archived tasks
navigateToTab("Tasks")
wait(seconds: 2)
// Scroll to archived column if it exists
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
scrollView.swipeLeft()
scrollView.swipeLeft()
}
// When: User unarchives a task
let unarchiveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Unarchive'")).firstMatch
if unarchiveButton.exists {
unarchiveButton.tap()
wait(seconds: 2)
// Then: Task should be restored from archive
}
}
// MARK: - Task Filtering and Viewing Tests
func testSwipeBetweenTaskColumns() {
// Given: User is viewing tasks
navigateToTab("Tasks")
wait(seconds: 2)
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
// When: User swipes to view different columns
scrollView.swipeLeft()
wait(seconds: 0.5)
// Then: Next column should be visible
scrollView.swipeLeft()
wait(seconds: 0.5)
// User can navigate between columns
scrollView.swipeRight()
wait(seconds: 0.5)
}
}
func testTasksByResidence() {
// Given: User is viewing a residence
navigateToTab("Residences")
wait(seconds: 2)
let firstResidence = app.cells.firstMatch
if firstResidence.exists {
firstResidence.tap()
wait(seconds: 2)
// Then: Tasks for that residence should be shown
let tasksSection = app.staticTexts["Tasks"]
XCTAssertTrue(tasksSection.exists || app.scrollViews.firstMatch.exists,
"Should show tasks section in residence details")
}
}
// MARK: - Task Recurrence Tests
func testCreateRecurringTask() {
// Given: User is creating a new task
navigateToTab("Tasks")
let addButton = app.navigationBars.buttons.matching(identifier: "plus").firstMatch
if addButton.exists {
addButton.tap()
wait(seconds: 1)
// When: User selects recurring frequency
let frequencyPicker = app.pickers.firstMatch
if frequencyPicker.exists {
// Select a frequency (e.g., Monthly)
let monthlyOption = app.pickerWheels.element.adjust(toPickerWheelValue: "Monthly")
// Task creation with recurrence
}
}
}
}

View File

@@ -0,0 +1,180 @@
import XCTest
/// Helper extensions and utilities for UI tests
extension XCUIApplication {
/// Launch the app and reset to initial state
func launchAndReset() {
launchArguments = ["--uitesting"]
launch()
}
/// Clear all text from a text field
func clearText(in textField: XCUIElement) {
textField.tap()
textField.press(forDuration: 1.2)
menuItems["Select All"].tap()
textField.typeText(XCUIKeyboardKey.delete.rawValue)
}
}
/// Base test class with common setup and teardown
class BaseUITest: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launchAndReset()
}
override func tearDown() {
app = nil
super.tearDown()
}
// MARK: - Authentication Helpers
func login(username: String, password: String) {
let usernameField = app.textFields["Username"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Login"]
if usernameField.exists {
usernameField.tap()
usernameField.typeText(username)
}
if passwordField.exists {
passwordField.tap()
passwordField.typeText(password)
}
if loginButton.exists {
loginButton.tap()
}
}
func logout() {
let profileTab = app.tabBars.buttons["Profile"]
profileTab.tap()
let logoutButton = app.buttons["Log Out"]
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5))
logoutButton.tap()
}
func register(username: String, email: String, password: String, firstName: String = "", lastName: String = "") {
let signUpButton = app.buttons["Sign Up"]
XCTAssertTrue(signUpButton.waitForExistence(timeout: 3))
signUpButton.tap()
let usernameField = app.textFields["Username"]
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let registerButton = app.buttons["Register"]
usernameField.tap()
usernameField.typeText(username)
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
if !firstName.isEmpty {
let firstNameField = app.textFields["First Name"]
firstNameField.tap()
firstNameField.typeText(firstName)
}
if !lastName.isEmpty {
let lastNameField = app.textFields["Last Name"]
lastNameField.tap()
lastNameField.typeText(lastName)
}
registerButton.tap()
}
// MARK: - Navigation Helpers
func navigateToTab(_ tabName: String) {
let tab = app.tabBars.buttons[tabName]
XCTAssertTrue(tab.waitForExistence(timeout: 3))
tab.tap()
}
func navigateBack() {
app.navigationBars.buttons.element(boundBy: 0).tap()
}
// MARK: - Assertion Helpers
func assertElementExists(_ identifier: String, timeout: TimeInterval = 5) {
let element = app.descendants(matching: .any).matching(identifier: identifier).firstMatch
XCTAssertTrue(element.waitForExistence(timeout: timeout), "Element '\(identifier)' does not exist")
}
func assertElementDoesNotExist(_ identifier: String, timeout: TimeInterval = 2) {
let element = app.descendants(matching: .any).matching(identifier: identifier).firstMatch
XCTAssertFalse(element.waitForExistence(timeout: timeout), "Element '\(identifier)' should not exist")
}
func assertNavigatedTo(title: String, timeout: TimeInterval = 5) {
let navigationBar = app.navigationBars[title]
XCTAssertTrue(navigationBar.waitForExistence(timeout: timeout), "Did not navigate to '\(title)'")
}
// MARK: - Wait Helpers
func wait(seconds: TimeInterval) {
Thread.sleep(forTimeInterval: seconds)
}
func waitForElementToAppear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
}
/// Identifiers for UI elements (for better testability)
struct Identifiers {
struct Authentication {
static let usernameField = "UsernameField"
static let passwordField = "PasswordField"
static let loginButton = "LoginButton"
static let registerButton = "RegisterButton"
static let logoutButton = "LogoutButton"
}
struct Residence {
static let addButton = "AddResidenceButton"
static let editButton = "EditResidenceButton"
static let deleteButton = "DeleteResidenceButton"
static let nameField = "ResidenceNameField"
static let addressField = "ResidenceAddressField"
}
struct Task {
static let addButton = "AddTaskButton"
static let completeButton = "CompleteTaskButton"
static let editButton = "EditTaskButton"
static let titleField = "TaskTitleField"
static let descriptionField = "TaskDescriptionField"
}
struct MultiUser {
static let manageUsersButton = "ManageUsersButton"
static let generateCodeButton = "GenerateCodeButton"
static let joinButton = "JoinResidenceButton"
static let shareCodeField = "ShareCodeField"
}
}