← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


Tests/UI/ListlessiOSUITests.swift (5455B)

1import XCTest
2
3final class ListlessiOSUITests: XCTestCase {
4    var app: XCUIApplication!
5
6    override func setUpWithError() throws {
7        continueAfterFailure = false
8        app = XCUIApplication()
9        app.launchArguments = ["UI_TESTING"]
10        app.launch()
11    }
12
13    override func tearDownWithError() throws {
14        app.terminate()
15    }
16
17    // MARK: - Helpers
18
19    /// The "Pull down to create" empty state label.
20    var emptyStateLabel: XCUIElement {
21        app.staticTexts["Pull down to create"]
22    }
23
24    /// The main scroll view area; tapping empty space here creates a draft item.
25    var itemListScrollView: XCUIElement {
26        app.scrollViews["item-list-scrollview"]
27    }
28
29    /// The draft row text view that appears after tapping empty space.
30    /// TappableTextField wraps UITextView, so XCUITest sees it as a textView.
31    var draftTextField: XCUIElement {
32        app.textViews.matching(identifier: "draft-row-append").firstMatch
33    }
34
35    /// Returns the static text for a committed item with the given title.
36    func itemText(_ title: String) -> XCUIElement {
37        app.staticTexts.matching(
38            NSPredicate(format: "identifier BEGINSWITH 'item-text-' AND label == %@", title)
39        ).firstMatch
40    }
41
42    /// Creates a item by tapping empty space to reveal the draft field,
43    /// typing a title, and pressing Return.
44    func createItem(_ title: String) {
45        let textView = draftTextField
46        if !textView.exists {
47            itemListScrollView.tap()
48            if !textView.waitForExistence(timeout: 2) {
49                XCTFail("Draft text view should appear after tapping empty space")
50                return
51            }
52        }
53        textView.tap()
54        textView.typeText(title + "\n")
55    }
56
57    /// Returns the Nth checkbox button (0-indexed).
58    func itemCheckbox(at index: Int) -> XCUIElement {
59        app.buttons.matching(identifier: "item-checkbox").element(boundBy: index)
60    }
61
62    /// Exits editing mode. After createItem, the new draft text view is
63    /// focused. Dismiss it by tapping the scroll view background (which
64    /// calls handleBackgroundTap to commit/dismiss the empty draft).
65    func exitEditingMode() {
66        let draft = draftTextField
67        if draft.exists {
68            draft.typeText("\n")
69        }
70        // Tap background to deselect
71        itemListScrollView.tap()
72    }
73
74    // MARK: - Empty State
75
76    func testLaunchShowsEmptyState() {
77        XCTAssertTrue(
78            emptyStateLabel.waitForExistence(timeout: 2),
79            "Empty state label should be visible on launch"
80        )
81    }
82
83    func testEmptyStateDisappearsAfterCreatingItem() {
84        createItem("First item")
85        exitEditingMode()
86        XCTAssertFalse(emptyStateLabel.exists, "Empty state should disappear after creating a item")
87    }
88
89    // MARK: - Item Creation
90
91    func testCreateItemViaTap() {
92        createItem("Buy groceries")
93        exitEditingMode()
94        XCTAssertTrue(
95            itemText("Buy groceries").waitForExistence(timeout: 2),
96            "Item should appear with the typed title"
97        )
98    }
99
100    func testReturnChainsNewItem() {
101        createItem("First item")
102        XCTAssertTrue(
103            draftTextField.waitForExistence(timeout: 2),
104            "New draft text view should appear after Return"
105        )
106    }
107
108    func testCreateMultipleItems() {
109        createItem("Alpha")
110        createItem("Bravo")
111        createItem("Charlie")
112        exitEditingMode()
113
114        XCTAssertTrue(itemText("Alpha").waitForExistence(timeout: 2))
115        XCTAssertTrue(itemText("Bravo").exists)
116        XCTAssertTrue(itemText("Charlie").exists)
117    }
118
119    func testEmptyItemDeletedOnCommit() {
120        itemListScrollView.tap()
121        XCTAssertTrue(draftTextField.waitForExistence(timeout: 2))
122        draftTextField.typeText("\n")
123        XCTAssertTrue(
124            emptyStateLabel.waitForExistence(timeout: 2),
125            "Empty state should reappear when empty item is discarded"
126        )
127    }
128
129    // MARK: - Item Completion
130
131    func testCompleteItemViaCheckbox() {
132        createItem("Finish report")
133        exitEditingMode()
134
135        let checkbox = itemCheckbox(at: 0)
136        XCTAssertTrue(checkbox.waitForExistence(timeout: 2))
137        XCTAssertEqual(checkbox.value as? String, "circle")
138        checkbox.tap()
139
140        let completed = app.buttons.matching(
141            NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
142        ).firstMatch
143        XCTAssertTrue(
144            completed.waitForExistence(timeout: 3),
145            "Checkbox should show checkmark after tapping"
146        )
147    }
148
149    func testUncompleteItem() {
150        createItem("Finish report")
151        exitEditingMode()
152        usleep(500_000)  // Wait for draft row reveal animation to settle
153
154        itemCheckbox(at: 0).tap()
155
156        let completed = app.buttons.matching(
157            NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
158        ).firstMatch
159        XCTAssertTrue(completed.waitForExistence(timeout: 3))
160        completed.tap()
161
162        let uncompleted = app.buttons.matching(
163            NSPredicate(format: "identifier == 'item-checkbox' AND value == 'circle'")
164        ).firstMatch
165        XCTAssertTrue(
166            uncompleted.waitForExistence(timeout: 3),
167            "Checkbox should revert to circle after uncompleting"
168        )
169    }
170}