912888f14c
Android UI Tests / ui-tests (push) Has been cancelled
- Issue 2 (coverage gap): add HoneyDueAPITests/AuthGatingAPITests — verifies the backend's RequireVerified gate (unverified -> 403, verified -> 200) at the API layer, since UI-test mode bypasses verification. NOTE: surfaced that the gate is applied to only the share-code routes, not residence/task routes — unverified users are NOT broadly blocked (flagged for product/backend). - Issue 4: TaskCRUDUITests seedAccountPreconditions now guarantees a residence (no silent early-return), so the cancelled-task precondition always populates; XCTUnwrap replaces the misleading "not seeded" skip. The two uncancel tests now skip with the ACCURATE reason: cancelled tasks are intentionally hidden from the Tasks Kanban and the iOS Tasks view has no "show cancelled" surface (product gap). - Issue 3: re-quarantine testF110 after a hardening attempt — the register->verify transition is irreducibly flaky; coverage is redundant with OnboardingTaskCache + the F-series. Skip reason is now precise, with a TODO to stabilize the handoff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
458 lines
21 KiB
Swift
458 lines
21 KiB
Swift
import XCTest
|
|
|
|
/// Task create/read/update/delete UI tests.
|
|
///
|
|
/// Merged from the former `Suite5_TaskTests` and `Tests/TaskIntegrationTests`.
|
|
/// Per-test isolation is provided by `AuthenticatedUITestCase`: every test mints
|
|
/// a fresh account, logs in, and tears it down. Task creation gates on a residence
|
|
/// existing, so `requiresResidence` seeds one BEFORE login (the fresh account is
|
|
/// otherwise empty and the Add-Task button would stay disabled).
|
|
///
|
|
/// Tests that must SEE a pre-existing task (uncancel flows) seed that task in
|
|
/// `seedAccountPreconditions` so the app loads it on its post-login fetch.
|
|
final class TaskCRUDUITests: AuthenticatedUITestCase {
|
|
|
|
// Task creation gates on a residence existing; seed one before login so the
|
|
// fresh account's app sees it (otherwise the Add-Task button stays disabled).
|
|
override var requiresResidence: Bool { true }
|
|
|
|
// MARK: - Preconditions
|
|
|
|
/// Cancelled task seeded before login for the uncancel flows. A fresh account
|
|
/// is empty at login, so a task seeded in the test body would be invisible to
|
|
/// the app without a manual refresh — seed it here instead.
|
|
private(set) var seededCancelledTask_uncancelFlow: TestTask?
|
|
private(set) var seededCancelledTask_uncancelV2: TestTask?
|
|
|
|
override func seedAccountPreconditions(_ account: TestAccount) {
|
|
super.seedAccountPreconditions(account) // seeds seededResidence (requiresResidence)
|
|
|
|
// A residence MUST exist before we can seed the cancelled tasks. The base
|
|
// populates `seededResidence` when `requiresResidence` is true, but rather
|
|
// than early-returning (and silently skipping the cancelled-task seeding —
|
|
// which then makes the uncancel tests SKIP instead of run), guarantee one
|
|
// here: fall back to seeding a residence directly if it's somehow nil.
|
|
let residence = seededResidence ?? account.seedResidence(name: "Precondition Home")
|
|
|
|
// TASK-010: a cancelled task that the test will uncancel/reopen.
|
|
// createCancelledTask is non-optional — it XCTFails (and crashes) on a real
|
|
// API failure, so a genuine break surfaces as a failure, never a silent skip.
|
|
seededCancelledTask_uncancelFlow = TestDataSeeder.createCancelledTask(
|
|
token: account.token,
|
|
residenceId: residence.id
|
|
)
|
|
|
|
// TASK-010 (v2): a named task, cancelled, that the test restores.
|
|
let v2Task = account.seedTask(
|
|
residenceId: residence.id,
|
|
title: "Uncancel Me \(Int(Date().timeIntervalSince1970))"
|
|
)
|
|
seededCancelledTask_uncancelV2 = TestAccountAPIClient.cancelTask(token: account.token, id: v2Task.id) ?? v2Task
|
|
}
|
|
|
|
/// Try to bring a task card into view on the Tasks Kanban by its title.
|
|
///
|
|
/// Refreshes via the toolbar button (the Kanban has no pull-to-refresh) and
|
|
/// swipes the board horizontally, returning the static-text element for the
|
|
/// card. NOTE: the backend intentionally HIDES cancelled and archived tasks
|
|
/// from `GET /tasks/` (the board's only data source — see the API's
|
|
/// `determineExpectedColumn`: cancelled/archived return "" = hidden). So a
|
|
/// seeded *cancelled* task will never surface here; callers must handle the
|
|
/// not-found case explicitly.
|
|
@discardableResult
|
|
private func revealKanbanTask(titled title: String, maxSwipes: Int = 6) -> XCUIElement {
|
|
let taskText = app.staticTexts[title]
|
|
|
|
refreshTasks()
|
|
if taskText.waitForExistence(timeout: defaultTimeout) { return taskText }
|
|
|
|
let board = app.scrollViews.firstMatch.exists
|
|
? app.scrollViews.firstMatch
|
|
: app.collectionViews.firstMatch
|
|
for _ in 0..<maxSwipes {
|
|
guard board.exists else { break }
|
|
board.swipeLeft()
|
|
if taskText.waitForExistence(timeout: 1.0) { return taskText }
|
|
}
|
|
return taskText
|
|
}
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
|
|
// Dismiss any open form from a previous test
|
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Task.formCancelButton].firstMatch
|
|
if cancelButton.exists { cancelButton.tap() }
|
|
|
|
navigateToTasks()
|
|
// Wait for task screen to load
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
addButton.waitForExistenceOrFail(timeout: navigationTimeout, message: "Task add button should appear")
|
|
}
|
|
|
|
// MARK: - Validation
|
|
|
|
func test01_cancelTaskCreation() {
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
addButton.tap()
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Task form should open")
|
|
|
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Task.formCancelButton].firstMatch
|
|
cancelButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Cancel button should exist")
|
|
cancelButton.tap()
|
|
|
|
// Verify we're back on the task list
|
|
let addButtonAgain = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
XCTAssertTrue(addButtonAgain.waitForExistence(timeout: navigationTimeout), "Should be back on tasks list after cancel")
|
|
}
|
|
|
|
// MARK: - View/List
|
|
|
|
func test02_tasksTabExists() {
|
|
let tabBar = app.tabBars.firstMatch
|
|
XCTAssertTrue(tabBar.exists, "Tab bar should exist")
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
XCTAssertTrue(addButton.exists, "Task add button should exist (proves we're on Tasks tab)")
|
|
}
|
|
|
|
func test03_viewTasksList() {
|
|
// Tasks screen should show — verified by the add button existence from setUp
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
XCTAssertTrue(addButton.exists, "Tasks screen should be visible with add button")
|
|
}
|
|
|
|
func test04_addTaskButtonEnabled() {
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
XCTAssertTrue(addButton.isEnabled, "Task add button should be enabled when residence exists")
|
|
}
|
|
|
|
func test05_navigateToAddTask() {
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
addButton.tap()
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Task title field should appear in add form")
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
|
|
XCTAssertTrue(saveButton.exists, "Save button should exist in add task form")
|
|
|
|
// Clean up: dismiss form
|
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Task.formCancelButton].firstMatch
|
|
if cancelButton.exists { cancelButton.tap() }
|
|
}
|
|
|
|
// MARK: - Creation
|
|
|
|
func test06_createBasicTask() {
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
addButton.tap()
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField].firstMatch
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Task title field should appear")
|
|
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let taskTitle = "UITest Task \(timestamp)"
|
|
fillTextField(identifier: AccessibilityIdentifiers.Task.titleField, text: taskTitle)
|
|
|
|
dismissKeyboard()
|
|
app.swipeUp()
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
|
|
saveButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Save button should exist")
|
|
saveButton.tap()
|
|
|
|
// Wait for form to dismiss
|
|
_ = saveButton.waitForNonExistence(timeout: navigationTimeout)
|
|
|
|
// Verify task was created via API (also gives the server time to process)
|
|
if let items = TestAccountAPIClient.listTasks(token: session.token),
|
|
let created = items.first(where: { $0.title.contains(taskTitle) }) {
|
|
cleaner.trackTask(created.id)
|
|
}
|
|
|
|
// Navigate to tasks tab and refresh to pick up the newly created task
|
|
navigateToTasks()
|
|
refreshTasks()
|
|
let taskListScreen = TaskListScreen(app: app)
|
|
let newTask = taskListScreen.findTask(title: taskTitle)
|
|
XCTAssertTrue(newTask.waitForExistence(timeout: loginTimeout), "New task '\(taskTitle)' should appear in the list")
|
|
}
|
|
|
|
func testTASK_CreateTaskAppearsInList() {
|
|
// Residence is seeded before login (requiresResidence) so task creation
|
|
// has a valid target.
|
|
navigateToTasks()
|
|
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
let emptyState = app.otherElements[AccessibilityIdentifiers.Task.emptyStateView]
|
|
let taskList = app.otherElements[AccessibilityIdentifiers.Task.tasksList]
|
|
|
|
let loaded = addButton.waitForExistence(timeout: defaultTimeout)
|
|
|| emptyState.waitForExistence(timeout: 3)
|
|
|| taskList.waitForExistence(timeout: 3)
|
|
XCTAssertTrue(loaded, "Tasks screen should load")
|
|
|
|
if addButton.exists && addButton.isHittable {
|
|
addButton.forceTap()
|
|
} else {
|
|
let emptyAddButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")
|
|
).firstMatch
|
|
emptyAddButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
emptyAddButton.forceTap()
|
|
}
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField]
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
let uniqueTitle = "IntTest Task \(Int(Date().timeIntervalSince1970))"
|
|
titleField.forceTap()
|
|
_ = app.keyboards.firstMatch.waitForExistence(timeout: 3)
|
|
titleField.typeText(uniqueTitle)
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton]
|
|
let scrollContainer = app.scrollViews.firstMatch.exists ? app.scrollViews.firstMatch : app.collectionViews.firstMatch
|
|
saveButton.scrollIntoView(in: scrollContainer)
|
|
saveButton.forceTap()
|
|
|
|
let newTask = app.staticTexts[uniqueTitle]
|
|
XCTAssertTrue(
|
|
newTask.waitForExistence(timeout: loginTimeout),
|
|
"Newly created task should appear"
|
|
)
|
|
}
|
|
|
|
// MARK: - View Details
|
|
|
|
func test07_viewTaskDetails() {
|
|
// Create a task first
|
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
let taskTitle = "UITest Detail \(timestamp)"
|
|
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
addButton.tap()
|
|
|
|
fillTextField(identifier: AccessibilityIdentifiers.Task.titleField, text: taskTitle)
|
|
dismissKeyboard()
|
|
app.swipeUp()
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton].firstMatch
|
|
saveButton.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
saveButton.tap()
|
|
_ = saveButton.waitForNonExistence(timeout: navigationTimeout)
|
|
|
|
// Verify task was created via API (also gives the server time to process)
|
|
if let items = TestAccountAPIClient.listTasks(token: session.token),
|
|
let created = items.first(where: { $0.title.contains(taskTitle) }) {
|
|
cleaner.trackTask(created.id)
|
|
}
|
|
|
|
// Navigate to tasks tab and refresh to pick up the newly created task
|
|
navigateToTasks()
|
|
refreshTasks()
|
|
let taskListScreen = TaskListScreen(app: app)
|
|
let taskCard = taskListScreen.findTask(title: taskTitle)
|
|
taskCard.waitForExistenceOrFail(timeout: loginTimeout, message: "Created task should appear in list")
|
|
|
|
// Verify the task card is accessible and the actions menu exists
|
|
// (There is no task detail screen — cards are self-contained with a context menu)
|
|
let actionsMenu = app.buttons["Task actions"].firstMatch
|
|
XCTAssertTrue(actionsMenu.waitForExistence(timeout: navigationTimeout), "Task actions menu should be accessible")
|
|
}
|
|
|
|
// MARK: - Navigation
|
|
|
|
func test08_navigateToContractors() {
|
|
navigateToContractors()
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Contractor.addButton].firstMatch
|
|
XCTAssertTrue(addButton.waitForExistence(timeout: navigationTimeout), "Contractors screen should load")
|
|
}
|
|
|
|
func test09_navigateToDocuments() {
|
|
navigateToDocuments()
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Document.addButton].firstMatch
|
|
XCTAssertTrue(addButton.waitForExistence(timeout: navigationTimeout), "Documents screen should load")
|
|
}
|
|
|
|
func test10_navigateBetweenTabs() {
|
|
navigateToResidences()
|
|
let resAddButton = app.buttons[AccessibilityIdentifiers.Residence.addButton].firstMatch
|
|
XCTAssertTrue(resAddButton.waitForExistence(timeout: navigationTimeout), "Residences screen should load")
|
|
|
|
navigateToTasks()
|
|
let taskAddButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
XCTAssertTrue(taskAddButton.waitForExistence(timeout: navigationTimeout), "Tasks screen should load after navigating back")
|
|
}
|
|
|
|
// MARK: - TASK-010: Uncancel Task
|
|
|
|
func testTASK010_UncancelTaskFlow() throws {
|
|
// Cancelled task was seeded BEFORE login (seedAccountPreconditions) so the
|
|
// app's post-login fetch already has it. Seeding is guaranteed (the
|
|
// precondition seeds a residence then a cancelled task, failing hard on a
|
|
// real API error), so a nil here is a genuine bug — surface it as a
|
|
// failure, not a skip.
|
|
let cancelledTask = try XCTUnwrap(
|
|
seededCancelledTask_uncancelFlow,
|
|
"Cancelled task precondition was not seeded — seedAccountPreconditions failed to populate it"
|
|
)
|
|
|
|
navigateToTasks()
|
|
|
|
// The cancelled task is seeded correctly (asserted above), but the backend
|
|
// intentionally hides cancelled/archived tasks from the Tasks Kanban
|
|
// (`GET /tasks/`) — the only view this tab exposes. There is currently no
|
|
// UI affordance to display, let alone uncancel, a cancelled task from the
|
|
// Tasks screen, so the flow cannot be exercised end-to-end here. Skip with
|
|
// the real reason (no longer the misleading "not seeded").
|
|
let taskText = revealKanbanTask(titled: cancelledTask.title)
|
|
guard taskText.waitForExistence(timeout: defaultTimeout) else {
|
|
throw XCTSkip("Cancelled tasks are hidden from the Tasks Kanban by design (backend omits cancelled/archived from GET /tasks/), so there is no UI surface to uncancel from. Seeding succeeded — see seedAccountPreconditions.")
|
|
}
|
|
taskText.forceTap()
|
|
|
|
// Look for an uncancel or reopen button
|
|
let uncancelButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Uncancel' OR label CONTAINS[c] 'Reopen' OR label CONTAINS[c] 'Restore'")
|
|
).firstMatch
|
|
|
|
if uncancelButton.waitForExistence(timeout: defaultTimeout) {
|
|
uncancelButton.forceTap()
|
|
|
|
let statusText = app.staticTexts.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Cancelled'")
|
|
).firstMatch
|
|
XCTAssertFalse(statusText.exists, "Task should no longer show as cancelled after uncancel")
|
|
}
|
|
}
|
|
|
|
// MARK: - TASK-010 (v2): Uncancel Task — Restores Cancelled Task to Active Lifecycle
|
|
|
|
func test15_uncancelRestorescancelledTask() throws {
|
|
// Residence + cancelled task were seeded BEFORE login
|
|
// (seedAccountPreconditions) so the app loads them on its post-login fetch.
|
|
// Seeding is guaranteed, so a nil here is a genuine bug — fail, don't skip.
|
|
let task = try XCTUnwrap(
|
|
seededCancelledTask_uncancelV2,
|
|
"Cancelled task precondition was not seeded — seedAccountPreconditions failed to populate it"
|
|
)
|
|
|
|
navigateToTasks()
|
|
|
|
// Seeding succeeded (asserted above), but the backend intentionally hides
|
|
// cancelled/archived tasks from the Tasks Kanban (`GET /tasks/`) — the only
|
|
// view this tab exposes — so there is no UI surface to uncancel from. Skip
|
|
// with the real reason (no longer the misleading "not seeded").
|
|
let taskText = revealKanbanTask(titled: task.title)
|
|
guard taskText.waitForExistence(timeout: defaultTimeout) else {
|
|
throw XCTSkip("Cancelled tasks are hidden from the Tasks Kanban by design (backend omits cancelled/archived from GET /tasks/), so there is no UI surface to uncancel from. Seeding succeeded — see seedAccountPreconditions.")
|
|
}
|
|
taskText.forceTap()
|
|
|
|
// Look for an uncancel / reopen / restore action
|
|
let uncancelButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Uncancel' OR label CONTAINS[c] 'Reopen' OR label CONTAINS[c] 'Restore'")
|
|
).firstMatch
|
|
|
|
uncancelButton.waitForExistenceOrFail(
|
|
timeout: defaultTimeout,
|
|
message: "Uncancel/Reopen/Restore action should be available on a cancelled task"
|
|
)
|
|
uncancelButton.forceTap()
|
|
|
|
// After uncancelling, the task should no longer show a Cancelled status label
|
|
let cancelledLabel = app.staticTexts.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Cancelled'")
|
|
).firstMatch
|
|
XCTAssertFalse(
|
|
cancelledLabel.waitForExistence(timeout: defaultTimeout),
|
|
"Task should no longer display 'Cancelled' status after being restored"
|
|
)
|
|
}
|
|
|
|
// MARK: - TASK-012: Delete Task
|
|
|
|
func testTASK012_DeleteTaskUpdatesViews() {
|
|
// Create a task via UI first (since Kanban board uses cached data).
|
|
// Residence is seeded before login (requiresResidence).
|
|
navigateToTasks()
|
|
|
|
let addButton = app.buttons[AccessibilityIdentifiers.Task.addButton].firstMatch
|
|
let emptyAddButton = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Add' OR label CONTAINS[c] 'Create'")
|
|
).firstMatch
|
|
|
|
let addVisible = addButton.waitForExistence(timeout: defaultTimeout) || emptyAddButton.waitForExistence(timeout: 3)
|
|
XCTAssertTrue(addVisible, "Add task button should be visible")
|
|
|
|
if addButton.exists && addButton.isHittable {
|
|
addButton.forceTap()
|
|
} else {
|
|
emptyAddButton.forceTap()
|
|
}
|
|
|
|
let titleField = app.textFields[AccessibilityIdentifiers.Task.titleField]
|
|
titleField.waitForExistenceOrFail(timeout: defaultTimeout)
|
|
let uniqueTitle = "Delete Task \(Int(Date().timeIntervalSince1970))"
|
|
titleField.forceTap()
|
|
_ = app.keyboards.firstMatch.waitForExistence(timeout: 3)
|
|
titleField.typeText(uniqueTitle)
|
|
|
|
let saveButton = app.buttons[AccessibilityIdentifiers.Task.saveButton]
|
|
let scrollContainer = app.scrollViews.firstMatch.exists ? app.scrollViews.firstMatch : app.collectionViews.firstMatch
|
|
if scrollContainer.exists {
|
|
saveButton.scrollIntoView(in: scrollContainer)
|
|
}
|
|
saveButton.forceTap()
|
|
|
|
// Wait for the task to appear in the Kanban board
|
|
let taskText = app.staticTexts[uniqueTitle]
|
|
taskText.waitForExistenceOrFail(timeout: loginTimeout)
|
|
|
|
// Tap the "Actions" menu on the task card to reveal cancel option
|
|
let actionsMenu = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Actions'")
|
|
).firstMatch
|
|
if actionsMenu.waitForExistence(timeout: defaultTimeout) {
|
|
actionsMenu.forceTap()
|
|
} else {
|
|
taskText.forceTap()
|
|
}
|
|
|
|
// Tap cancel (tasks use "Cancel Task" semantics)
|
|
let deleteButton = app.buttons[AccessibilityIdentifiers.Task.deleteButton]
|
|
if !deleteButton.waitForExistence(timeout: defaultTimeout) {
|
|
let cancelTask = app.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Cancel Task'")
|
|
).firstMatch
|
|
cancelTask.waitForExistenceOrFail(timeout: 5)
|
|
cancelTask.forceTap()
|
|
} else {
|
|
deleteButton.forceTap()
|
|
}
|
|
|
|
// Confirm cancellation
|
|
let confirmDelete = app.alerts.buttons.containing(
|
|
NSPredicate(format: "label CONTAINS[c] 'Confirm' OR label CONTAINS[c] 'Yes' OR label CONTAINS[c] 'Cancel Task'")
|
|
).firstMatch
|
|
let alertConfirmButton = app.buttons[AccessibilityIdentifiers.Alert.confirmButton]
|
|
|
|
if alertConfirmButton.waitForExistence(timeout: defaultTimeout) {
|
|
alertConfirmButton.tap()
|
|
} else if confirmDelete.waitForExistence(timeout: defaultTimeout) {
|
|
confirmDelete.tap()
|
|
}
|
|
|
|
// Refresh the task list (kanban uses toolbar button, not pull-to-refresh)
|
|
refreshTasks()
|
|
|
|
// Verify the task is removed or moved to a different column
|
|
let deletedTask = app.staticTexts[uniqueTitle]
|
|
XCTAssertTrue(
|
|
deletedTask.waitForNonExistence(timeout: loginTimeout),
|
|
"Cancelled task should no longer appear in active views"
|
|
)
|
|
}
|
|
}
|