wip
This commit is contained in:
@@ -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
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)) }
|
||||
|
||||
@@ -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
|
||||
)) }
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? "")
|
||||
}
|
||||
|
||||
|
||||
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