wip
This commit is contained in:
@@ -423,7 +423,7 @@ fun App() {
|
|||||||
description = route.description,
|
description = route.description,
|
||||||
category = TaskCategory(route.categoryId, route.categoryName),
|
category = TaskCategory(route.categoryId, route.categoryName),
|
||||||
frequency = TaskFrequency(
|
frequency = TaskFrequency(
|
||||||
route.frequencyId, route.frequencyName, "",
|
route.frequencyId, route.frequencyName, "", route.frequencyName,
|
||||||
daySpan = 0,
|
daySpan = 0,
|
||||||
notifyDays = 0
|
notifyDays = 0
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ data class CustomTask (
|
|||||||
val category: String,
|
val category: String,
|
||||||
val priority: String,
|
val priority: String,
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String?,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
||||||
@SerialName("actual_cost") val actualCost: String? = null,
|
@SerialName("actual_cost") val actualCost: String? = null,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
@@ -62,7 +62,7 @@ data class TaskDetail(
|
|||||||
val priority: TaskPriority,
|
val priority: TaskPriority,
|
||||||
val frequency: TaskFrequency,
|
val frequency: TaskFrequency,
|
||||||
val status: TaskStatus?,
|
val status: TaskStatus?,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String?,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
||||||
@SerialName("actual_cost") val actualCost: String? = null,
|
@SerialName("actual_cost") val actualCost: String? = null,
|
||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ data class TaskFrequencyResponse(
|
|||||||
data class TaskFrequency(
|
data class TaskFrequency(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@SerialName("lookup_name") val lookupName: String,
|
||||||
@SerialName("display_name") val displayName: String,
|
@SerialName("display_name") val displayName: String,
|
||||||
@SerialName("day_span") val daySpan: Int? = null,
|
@SerialName("day_span") val daySpan: Int? = null,
|
||||||
@SerialName("notify_days") val notifyDays: Int? = null
|
@SerialName("notify_days") val notifyDays: Int? = null
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ data class EditTaskRoute(
|
|||||||
val priorityName: String,
|
val priorityName: String,
|
||||||
val statusId: Int?,
|
val statusId: Int?,
|
||||||
val statusName: String?,
|
val statusName: String?,
|
||||||
val dueDate: String,
|
val dueDate: String?,
|
||||||
val estimatedCost: String?,
|
val estimatedCost: String?,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fun AddNewTaskDialog(
|
|||||||
|
|
||||||
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
||||||
var frequency by remember { mutableStateOf(TaskFrequency(
|
var frequency by remember { mutableStateOf(TaskFrequency(
|
||||||
id = 0, name = "", displayName = "",
|
id = 0, name = "", lookupName = "", displayName = "",
|
||||||
daySpan = 0,
|
daySpan = 0,
|
||||||
notifyDays = 0
|
notifyDays = 0
|
||||||
)) }
|
)) }
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun AddNewTaskWithResidenceDialog(
|
|||||||
var selectedResidenceId by remember { mutableStateOf(residencesResponse.residences.firstOrNull()?.id ?: 0) }
|
var selectedResidenceId by remember { mutableStateOf(residencesResponse.residences.firstOrNull()?.id ?: 0) }
|
||||||
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
||||||
var frequency by remember { mutableStateOf(TaskFrequency(
|
var frequency by remember { mutableStateOf(TaskFrequency(
|
||||||
id = 0, name = "", displayName = "",
|
id = 0, name = "", lookupName = "", displayName = "",
|
||||||
daySpan = 0,
|
daySpan = 0,
|
||||||
notifyDays = 0
|
notifyDays = 0
|
||||||
)) }
|
)) }
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ fun TaskCard(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = task.nextScheduledDate ?: task.dueDate,
|
text = task.nextScheduledDate ?: task.dueDate ?: "N/A",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
@@ -345,7 +345,7 @@ fun TaskCardPreview() {
|
|||||||
category = TaskCategory(id = 1, name = "maintenance", description = ""),
|
category = TaskCategory(id = 1, name = "maintenance", description = ""),
|
||||||
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
|
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
|
||||||
frequency = TaskFrequency(
|
frequency = TaskFrequency(
|
||||||
id = 1, name = "monthly", displayName = "Monthly",
|
id = 1, name = "monthly", lookupName = "monthly", displayName = "Monthly",
|
||||||
daySpan = 0,
|
daySpan = 0,
|
||||||
notifyDays = 0
|
notifyDays = 0
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fun EditTaskScreen(
|
|||||||
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
|
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
|
||||||
var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) }
|
var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) }
|
||||||
var selectedStatus by remember { mutableStateOf<TaskStatus?>(task.status) }
|
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 estimatedCost by remember { mutableStateOf(task.estimatedCost ?: "") }
|
||||||
|
|
||||||
var categoryExpanded by remember { mutableStateOf(false) }
|
var categoryExpanded by remember { mutableStateOf(false) }
|
||||||
@@ -70,7 +70,7 @@ fun EditTaskScreen(
|
|||||||
titleError = ""
|
titleError = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dueDate.isBlank()) {
|
if (dueDate.isNullOrBlank()) {
|
||||||
dueDateError = "Due date is required"
|
dueDateError = "Due date is required"
|
||||||
isValid = false
|
isValid = false
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ struct TaskCard: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Label(formatDate(task.dueDate), systemImage: "calendar")
|
if let due_date = task.dueDate {
|
||||||
.font(.caption)
|
Label(formatDate(due_date), systemImage: "calendar")
|
||||||
.foregroundColor(.secondary)
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.completions.count > 0 {
|
if task.completions.count > 0 {
|
||||||
@@ -169,7 +171,7 @@ struct TaskCard: View {
|
|||||||
description: "Remove all debris from gutters",
|
description: "Remove all debris from gutters",
|
||||||
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
||||||
priority: TaskPriority(id: 2, name: "medium", displayName: "", 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: ""),
|
status: TaskStatus(id: 1, name: "pending", displayName: "", description: ""),
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
estimatedCost: "150.00",
|
estimatedCost: "150.00",
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ struct TasksSection: View {
|
|||||||
description: "Remove all debris",
|
description: "Remove all debris",
|
||||||
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
||||||
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", 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: ""),
|
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: ""),
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
estimatedCost: "150.00",
|
estimatedCost: "150.00",
|
||||||
@@ -115,7 +115,7 @@ struct TasksSection: View {
|
|||||||
description: "Kitchen sink fixed",
|
description: "Kitchen sink fixed",
|
||||||
category: TaskCategory(id: 2, name: "plumbing", description: ""),
|
category: TaskCategory(id: 2, name: "plumbing", description: ""),
|
||||||
priority: TaskPriority(id: 3, name: "high", displayName: "High", 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: ""),
|
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: ""),
|
||||||
dueDate: "2024-11-01",
|
dueDate: "2024-11-01",
|
||||||
estimatedCost: "200.00",
|
estimatedCost: "200.00",
|
||||||
|
|||||||
@@ -321,9 +321,11 @@ struct DynamicTaskCard: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Label(formatDate(task.dueDate), systemImage: "calendar")
|
if let due_date = task.dueDate {
|
||||||
.font(.caption)
|
Label(formatDate(due_date), systemImage: "calendar")
|
||||||
.foregroundColor(.secondary)
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.completions.count > 0 {
|
if task.completions.count > 0 {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct EditTaskView: View {
|
|||||||
_selectedFrequency = State(initialValue: task.frequency)
|
_selectedFrequency = State(initialValue: task.frequency)
|
||||||
_selectedPriority = State(initialValue: task.priority)
|
_selectedPriority = State(initialValue: task.priority)
|
||||||
_selectedStatus = State(initialValue: task.status)
|
_selectedStatus = State(initialValue: task.status)
|
||||||
_dueDate = State(initialValue: task.dueDate)
|
_dueDate = State(initialValue: task.dueDate ?? "")
|
||||||
_estimatedCost = State(initialValue: task.estimatedCost ?? "")
|
_estimatedCost = State(initialValue: task.estimatedCost ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
269
iosApp/iosAppUITests/AuthenticationUITests.swift
Normal file
269
iosApp/iosAppUITests/AuthenticationUITests.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
470
iosApp/iosAppUITests/MultiUserUITests.swift
Normal file
470
iosApp/iosAppUITests/MultiUserUITests.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
346
iosApp/iosAppUITests/ResidenceUITests.swift
Normal file
346
iosApp/iosAppUITests/ResidenceUITests.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
431
iosApp/iosAppUITests/TaskUITests.swift
Normal file
431
iosApp/iosAppUITests/TaskUITests.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
iosApp/iosAppUITests/TestHelpers.swift
Normal file
180
iosApp/iosAppUITests/TestHelpers.swift
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user