Add residence picker to contractor create/edit screens
Kotlin/KMM: - Update Contractor model with optional residenceId and specialties array - Rename averageRating to rating, update address field names - Add ContractorMinimal model for task references - Add residence picker and multi-select specialty chips to AddContractorDialog - Fix ContractorsScreen and ContractorDetailScreen field references iOS: - Rewrite ContractorFormSheet with residence and specialty pickers - Update ContractorDetailView with FlowLayout for specialties - Add FlowLayout component for wrapping badge layouts - Fix ContractorCard and CompleteTaskView field references - Update ContractorFormState with residence/specialty selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
376
iosApp/CaseraUITests/Suite5_TaskTests.swift
Normal file
376
iosApp/CaseraUITests/Suite5_TaskTests.swift
Normal file
@@ -0,0 +1,376 @@
|
||||
import XCTest
|
||||
|
||||
/// Task management tests
|
||||
/// Uses UITestHelpers for consistent login/logout behavior
|
||||
/// IMPORTANT: Tasks require at least one residence to exist
|
||||
///
|
||||
/// Test Order (least to most complex):
|
||||
/// 1. Error/incomplete data tests
|
||||
/// 2. Creation tests
|
||||
/// 3. Edit/update tests
|
||||
/// 4. Delete/remove tests (none currently)
|
||||
/// 5. Navigation/view tests
|
||||
final class Suite5_TaskTests: XCTestCase {
|
||||
var app: XCUIApplication!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Ensure user is logged in
|
||||
UITestHelpers.ensureLoggedIn(app: app)
|
||||
|
||||
// CRITICAL: Ensure at least one residence exists
|
||||
// Tasks are disabled if no residences exist
|
||||
ensureResidenceExists()
|
||||
|
||||
// Now navigate to Tasks tab
|
||||
navigateToTasksTab()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
app = nil
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Ensures at least one residence exists (required for tasks to work)
|
||||
private func ensureResidenceExists() {
|
||||
// Navigate to Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
if residencesTab.waitForExistence(timeout: 5) {
|
||||
residencesTab.tap()
|
||||
sleep(2)
|
||||
|
||||
// Check if we have any residences
|
||||
// Look for the add button - if we see "Add a property" text or empty state, create one
|
||||
let emptyStateText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'No properties' OR label CONTAINS[c] 'No residences'")).firstMatch
|
||||
|
||||
if emptyStateText.exists {
|
||||
// No residences exist, create a quick one
|
||||
let addButton = app.buttons[AccessibilityIdentifiers.Residence.addButton]
|
||||
if addButton.waitForExistence(timeout: 5) {
|
||||
addButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Fill minimal required fields
|
||||
let nameField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Name'")).firstMatch
|
||||
if nameField.waitForExistence(timeout: 5) {
|
||||
nameField.tap()
|
||||
nameField.typeText("Test Home for Tasks")
|
||||
|
||||
// Scroll to address fields
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// Fill required address fields
|
||||
let streetField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Street'")).firstMatch
|
||||
if streetField.exists {
|
||||
streetField.tap()
|
||||
streetField.typeText("123 Test St")
|
||||
}
|
||||
|
||||
let cityField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'City'")).firstMatch
|
||||
if cityField.exists {
|
||||
cityField.tap()
|
||||
cityField.typeText("TestCity")
|
||||
}
|
||||
|
||||
let stateField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'State'")).firstMatch
|
||||
if stateField.exists {
|
||||
stateField.tap()
|
||||
stateField.typeText("TS")
|
||||
}
|
||||
|
||||
let postalField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Postal' OR placeholderValue CONTAINS[c] 'Zip'")).firstMatch
|
||||
if postalField.exists {
|
||||
postalField.tap()
|
||||
postalField.typeText("12345")
|
||||
}
|
||||
|
||||
// Save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
if saveButton.exists {
|
||||
saveButton.tap()
|
||||
sleep(3) // Wait for save to complete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToTasksTab() {
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
if tasksTab.waitForExistence(timeout: 5) {
|
||||
if !tasksTab.isSelected {
|
||||
tasksTab.tap()
|
||||
sleep(3) // Give it time to load
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the Add Task button using multiple strategies
|
||||
/// The button exists in two places:
|
||||
/// 1. Toolbar (always visible when residences exist)
|
||||
/// 2. Empty state (visible when no tasks exist)
|
||||
private func findAddTaskButton() -> XCUIElement {
|
||||
sleep(2) // Wait for screen to fully render
|
||||
|
||||
// Strategy 1: Try accessibility identifier
|
||||
let addButtonById = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
||||
if addButtonById.exists && addButtonById.isEnabled {
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// Strategy 2: Look for toolbar add button (navigation bar plus button)
|
||||
let navBarButtons = app.navigationBars.buttons
|
||||
for i in 0..<navBarButtons.count {
|
||||
let button = navBarButtons.element(boundBy: i)
|
||||
if button.label == "plus" || button.label.contains("Add") {
|
||||
if button.isEnabled {
|
||||
return button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try finding "Add Task" button in empty state by text
|
||||
let emptyStateButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Add Task'")).firstMatch
|
||||
if emptyStateButton.exists && emptyStateButton.isEnabled {
|
||||
return emptyStateButton
|
||||
}
|
||||
|
||||
// Strategy 4: Look for any enabled button with a plus icon
|
||||
let allButtons = app.buttons
|
||||
for i in 0..<min(allButtons.count, 20) { // Check first 20 buttons
|
||||
let button = allButtons.element(boundBy: i)
|
||||
if button.isEnabled && (button.label.contains("plus") || button.label.contains("Add")) {
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
||||
// Return the identifier one as fallback (will fail assertion if doesn't exist)
|
||||
return addButtonById
|
||||
}
|
||||
|
||||
// MARK: - 1. Error/Validation Tests
|
||||
|
||||
func test01_cancelTaskCreation() {
|
||||
// Given: User is on add task form
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify form opened
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task form should open")
|
||||
|
||||
// When: User taps cancel
|
||||
let cancelButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Cancel'")).firstMatch
|
||||
XCTAssertTrue(cancelButton.exists, "Cancel button should exist in task form")
|
||||
cancelButton.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should return to tasks list
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after cancel")
|
||||
}
|
||||
|
||||
// MARK: - 2. View/List Tests
|
||||
|
||||
func test02_tasksTabExists() {
|
||||
// Given: User is logged in
|
||||
// When: User looks for Tasks tab
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
|
||||
// Then: Tasks tab should exist
|
||||
XCTAssertTrue(tasksTab.exists, "Tasks tab should exist in main tab bar")
|
||||
XCTAssertTrue(tasksTab.isSelected, "Tasks tab should be selected after navigation")
|
||||
}
|
||||
|
||||
func test03_viewTasksList() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Then: Tasks screen should be visible
|
||||
// Verify we're on the right screen by checking for the navigation title
|
||||
let tasksTitle = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'All Tasks' OR label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTitle.waitForExistence(timeout: 5), "Tasks screen title should be visible")
|
||||
}
|
||||
|
||||
func test04_addTaskButtonExists() {
|
||||
// Given: User is on Tasks tab with at least one residence
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Then: Add task button should exist and be enabled
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists, "Add task button should exist on Tasks screen")
|
||||
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled when residence exists")
|
||||
}
|
||||
|
||||
func test05_navigateToAddTask() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// When: User taps add task button
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists, "Add task button should exist")
|
||||
XCTAssertTrue(addButton.isEnabled, "Add task button should be enabled")
|
||||
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Then: Should show add task form with required fields
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title' OR placeholderValue CONTAINS[c] 'Task'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear in add form")
|
||||
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist in add task form")
|
||||
}
|
||||
|
||||
// MARK: - 3. Creation Tests
|
||||
|
||||
func test06_createBasicTask() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// When: User taps add task button
|
||||
let addButton = findAddTaskButton()
|
||||
XCTAssertTrue(addButton.exists && addButton.isEnabled, "Add task button should exist and be enabled")
|
||||
addButton.tap()
|
||||
sleep(3)
|
||||
|
||||
// Verify task form loaded
|
||||
let titleField = app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Title'")).firstMatch
|
||||
XCTAssertTrue(titleField.waitForExistence(timeout: 5), "Task title field should appear")
|
||||
|
||||
// Fill in task title with unique timestamp
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let taskTitle = "UITest Task \(timestamp)"
|
||||
titleField.tap()
|
||||
titleField.typeText(taskTitle)
|
||||
|
||||
// Scroll down to find and fill description
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
let descField = app.textViews.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'Description'")).firstMatch
|
||||
if descField.exists {
|
||||
descField.tap()
|
||||
descField.typeText("Test task")
|
||||
}
|
||||
|
||||
// Scroll to find Save button
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps save
|
||||
let saveButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Save'")).firstMatch
|
||||
XCTAssertTrue(saveButton.exists, "Save button should exist")
|
||||
saveButton.tap()
|
||||
|
||||
// Then: Should return to tasks list
|
||||
sleep(5) // Wait for API call to complete
|
||||
|
||||
// Verify we're back on tasks list by checking tab exists
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
XCTAssertTrue(tasksTab.exists, "Should be back on tasks list after saving")
|
||||
|
||||
// Verify task appears in the list (may be in kanban columns)
|
||||
let newTask = app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(taskTitle)'")).firstMatch
|
||||
XCTAssertTrue(newTask.waitForExistence(timeout: 10), "New task '\(taskTitle)' should appear in the list")
|
||||
}
|
||||
|
||||
// MARK: - 4. View Details Tests
|
||||
|
||||
func test07_viewTaskDetails() {
|
||||
// Given: User is on Tasks tab and at least one task exists
|
||||
navigateToTasksTab()
|
||||
sleep(3)
|
||||
|
||||
// Look for any task in the list
|
||||
let taskCard = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'UITest Task' OR label CONTAINS 'Test'")).firstMatch
|
||||
|
||||
if !taskCard.waitForExistence(timeout: 5) {
|
||||
// No task found - skip this test
|
||||
print("No tasks found - run testCreateBasicTask first")
|
||||
return
|
||||
}
|
||||
|
||||
// When: User taps on a task
|
||||
taskCard.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should show task details screen
|
||||
let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Edit'")).firstMatch
|
||||
let completeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Complete' OR label CONTAINS[c] 'Mark'")).firstMatch
|
||||
let backButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Back' OR label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
|
||||
let detailScreenVisible = editButton.exists || completeButton.exists || backButton.exists
|
||||
XCTAssertTrue(detailScreenVisible, "Task details screen should show with action buttons")
|
||||
}
|
||||
|
||||
// MARK: - 5. Navigation Tests
|
||||
|
||||
func test08_navigateToContractors() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps Contractors tab
|
||||
let contractorsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
|
||||
XCTAssertTrue(contractorsTab.waitForExistence(timeout: 5), "Contractors tab should exist")
|
||||
contractorsTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Contractors tab
|
||||
XCTAssertTrue(contractorsTab.isSelected, "Contractors tab should be selected")
|
||||
}
|
||||
|
||||
func test09_navigateToDocuments() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User taps Documents tab
|
||||
let documentsTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Documents'")).firstMatch
|
||||
XCTAssertTrue(documentsTab.waitForExistence(timeout: 5), "Documents tab should exist")
|
||||
documentsTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Documents tab
|
||||
XCTAssertTrue(documentsTab.isSelected, "Documents tab should be selected")
|
||||
}
|
||||
|
||||
func test10_navigateBetweenTabs() {
|
||||
// Given: User is on Tasks tab
|
||||
navigateToTasksTab()
|
||||
sleep(1)
|
||||
|
||||
// When: User navigates to Residences tab
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.exists, "Residences tab should exist")
|
||||
residencesTab.tap()
|
||||
sleep(1)
|
||||
|
||||
// Then: Should be on Residences tab
|
||||
XCTAssertTrue(residencesTab.isSelected, "Should be on Residences tab")
|
||||
|
||||
// When: User navigates back to Tasks
|
||||
let tasksTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
|
||||
tasksTab.tap()
|
||||
sleep(2)
|
||||
|
||||
// Then: Should be back on Tasks tab
|
||||
XCTAssertTrue(tasksTab.isSelected, "Should be back on Tasks tab")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user