Tests/UI/ListlessMacUITests.swift (16143B)
1import CoreGraphics
2import XCTest
3
4final class ListlessMacUITests: XCTestCase {
5 var app: XCUIApplication!
6
7 override func setUpWithError() throws {
8 continueAfterFailure = false
9 app = XCUIApplication()
10 app.launchArguments = ["UI_TESTING"]
11 app.launch()
12 }
13
14 override func tearDownWithError() throws {
15 app.terminate()
16 }
17
18
19
20
21 var emptyStateLabel: XCUIElement {
22 app.staticTexts["Click to create"]
23 }
24
25
26 var draftTextField: XCUIElement {
27 app.textFields["draft-row-append"]
28 }
29
30
31 func itemText(_ title: String) -> XCUIElement {
32 app.textFields.matching(
33 NSPredicate(format: "identifier BEGINSWITH 'item-text-' AND value == %@", title)
34 ).firstMatch
35 }
36
37
38
39 func createItem(_ title: String) {
40 let textField = draftTextField
41 if !textField.exists {
42 app.typeKey("n", modifierFlags: .command)
43 if !textField.waitForExistence(timeout: 2) {
44 XCTFail("Draft text field should appear after Cmd+N")
45 return
46 }
47 }
48 textField.click()
49 textField.typeText(title)
50 textField.typeKey(.return, modifierFlags: [])
51 }
52
53
54 func itemCheckbox(at index: Int) -> XCUIElement {
55 app.buttons.matching(identifier: "item-checkbox").element(boundBy: index)
56 }
57
58
59
60 func navigateToItem(at index: Int) {
61 app.typeKey(.escape, modifierFlags: [])
62 for _ in 0...index {
63 app.typeKey(.downArrow, modifierFlags: [])
64 }
65 }
66
67
68
69
70
71
72 func cmdClickRow(withText title: String) {
73 let textField = itemText(title)
74 XCTAssertTrue(textField.waitForExistence(timeout: 2))
75
76 let coord = textField.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0.5))
77 .withOffset(CGVector(dx: -40, dy: 0))
78 let point = coord.screenPoint
79
80 let src = CGEventSource(stateID: .combinedSessionState)
81
82 let mouseDown = CGEvent(
83 mouseEventSource: src, mouseType: .leftMouseDown,
84 mouseCursorPosition: point, mouseButton: .left
85 )!
86 mouseDown.flags = .maskCommand
87 mouseDown.post(tap: .cgSessionEventTap)
88 usleep(50_000)
89
90 let mouseUp = CGEvent(
91 mouseEventSource: src, mouseType: .leftMouseUp,
92 mouseCursorPosition: point, mouseButton: .left
93 )!
94 mouseUp.flags = .maskCommand
95 mouseUp.post(tap: .cgSessionEventTap)
96 usleep(200_000)
97 }
98
99
100
101 func testLaunchShowsEmptyState() {
102 XCTAssertTrue(
103 emptyStateLabel.waitForExistence(timeout: 2),
104 "Empty state label should be visible on launch"
105 )
106 }
107
108 func testEmptyStateDisappearsAfterCreatingItem() {
109 createItem("First item")
110 app.typeKey(.escape, modifierFlags: [])
111 XCTAssertFalse(emptyStateLabel.exists, "Empty state should disappear after creating a item")
112 }
113
114
115
116 func testCmdNFocusesDraftField() {
117 app.typeKey("n", modifierFlags: .command)
118 let textField = draftTextField
119 XCTAssertTrue(
120 textField.waitForExistence(timeout: 2),
121 "Draft text field should appear after Cmd+N"
122 )
123
124 textField.typeText("Focused item")
125 textField.typeKey(.return, modifierFlags: [])
126 XCTAssertTrue(
127 itemText("Focused item").waitForExistence(timeout: 2),
128 "Typing without clicking should commit the item if draft field has focus"
129 )
130 }
131
132 func testCreateItemViaMenuShortcut() {
133 createItem("Buy groceries")
134 XCTAssertTrue(
135 itemText("Buy groceries").waitForExistence(timeout: 2),
136 "Item should appear with the typed title"
137 )
138 }
139
140 func testReturnChainsNewItem() {
141 createItem("First item")
142 XCTAssertTrue(
143 draftTextField.waitForExistence(timeout: 2),
144 "New draft text field should appear after Return"
145 )
146 }
147
148 func testCreateMultipleItems() {
149 createItem("Alpha")
150 createItem("Bravo")
151 createItem("Charlie")
152 app.typeKey(.escape, modifierFlags: [])
153
154 XCTAssertTrue(itemText("Alpha").waitForExistence(timeout: 2))
155 XCTAssertTrue(itemText("Bravo").exists)
156 XCTAssertTrue(itemText("Charlie").exists)
157 }
158
159 func testEmptyItemDeletedOnCommit() {
160 app.typeKey("n", modifierFlags: .command)
161 XCTAssertTrue(draftTextField.waitForExistence(timeout: 2))
162 app.typeKey(.escape, modifierFlags: [])
163 XCTAssertTrue(
164 emptyStateLabel.waitForExistence(timeout: 2),
165 "Empty state should reappear when empty item is discarded"
166 )
167 }
168
169
170
171 func testCompleteItemViaCheckbox() {
172 createItem("Finish report")
173 app.typeKey(.escape, modifierFlags: [])
174
175 let checkbox = itemCheckbox(at: 0)
176 XCTAssertTrue(checkbox.waitForExistence(timeout: 2))
177 XCTAssertEqual(checkbox.value as? String, "circle")
178 checkbox.click()
179
180 let completed = app.buttons.matching(
181 NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
182 ).firstMatch
183 XCTAssertTrue(
184 completed.waitForExistence(timeout: 3),
185 "Checkbox should show checkmark after clicking"
186 )
187 }
188
189 func testUncompleteItem() {
190 createItem("Finish report")
191 app.typeKey(.escape, modifierFlags: [])
192
193 itemCheckbox(at: 0).click()
194
195 let completed = app.buttons.matching(
196 NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
197 ).firstMatch
198 XCTAssertTrue(completed.waitForExistence(timeout: 3))
199 completed.click()
200
201 let uncompleted = app.buttons.matching(
202 NSPredicate(format: "identifier == 'item-checkbox' AND value == 'circle'")
203 ).firstMatch
204 XCTAssertTrue(
205 uncompleted.waitForExistence(timeout: 3),
206 "Checkbox should revert to circle after uncompleting"
207 )
208 }
209
210
211
212 func testDeleteItemViaBackspace() {
213 createItem("Delete me")
214 navigateToItem(at: 0)
215 app.typeKey(.delete, modifierFlags: [])
216 XCTAssertTrue(
217 emptyStateLabel.waitForExistence(timeout: 2),
218 "Empty state should reappear after deleting the only item"
219 )
220 }
221
222 func testArrowKeyNavigationThenDelete() {
223 createItem("Keep me")
224 createItem("Delete me")
225 navigateToItem(at: 1)
226 app.typeKey(.delete, modifierFlags: [])
227
228 XCTAssertTrue(itemText("Keep me").waitForExistence(timeout: 2), "First item should remain")
229 XCTAssertFalse(itemText("Delete me").exists, "Second item should be deleted")
230 }
231
232
233
234 func testMoveItemUp() {
235 createItem("Alpha")
236 createItem("Bravo")
237 navigateToItem(at: 1)
238 app.typeKey(.upArrow, modifierFlags: .command)
239
240 let bravo = itemText("Bravo")
241 let alpha = itemText("Alpha")
242 XCTAssertTrue(bravo.waitForExistence(timeout: 2))
243 XCTAssertTrue(alpha.exists)
244 XCTAssertLessThan(
245 bravo.frame.minY, alpha.frame.minY,
246 "Bravo should appear above Alpha after moving up"
247 )
248 }
249
250 func testMoveItemDown() {
251 createItem("Alpha")
252 createItem("Bravo")
253 navigateToItem(at: 0)
254 app.typeKey(.downArrow, modifierFlags: .command)
255
256 let alpha = itemText("Alpha")
257 let bravo = itemText("Bravo")
258 XCTAssertTrue(alpha.waitForExistence(timeout: 2))
259 XCTAssertTrue(bravo.exists)
260 XCTAssertGreaterThan(
261 alpha.frame.minY, bravo.frame.minY,
262 "Alpha should appear below Bravo after moving down"
263 )
264 }
265
266
267
268 func testSelectAllThenDelete() {
269 createItem("Alpha")
270 createItem("Bravo")
271 createItem("Charlie")
272 navigateToItem(at: 0)
273 app.typeKey("a", modifierFlags: .command)
274 app.typeKey(.delete, modifierFlags: [])
275 XCTAssertTrue(
276 emptyStateLabel.waitForExistence(timeout: 2),
277 "All items should be deleted after Select All + Delete"
278 )
279 }
280
281 func testSelectAllIncludesCompletedItems() {
282 createItem("Active item")
283 createItem("Done item")
284 app.typeKey(.escape, modifierFlags: [])
285
286
287 itemCheckbox(at: 1).click()
288 let completed = app.buttons.matching(
289 NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
290 ).firstMatch
291 XCTAssertTrue(completed.waitForExistence(timeout: 3))
292
293
294 navigateToItem(at: 0)
295 app.typeKey("a", modifierFlags: .command)
296 app.typeKey(.delete, modifierFlags: [])
297 XCTAssertTrue(
298 emptyStateLabel.waitForExistence(timeout: 2),
299 "Both active and completed items should be deleted after Select All + Delete"
300 )
301 }
302
303
304
305 func testClearCompleted() {
306 createItem("Done item")
307 app.typeKey(.escape, modifierFlags: [])
308
309 itemCheckbox(at: 0).click()
310
311 let completed = app.buttons.matching(
312 NSPredicate(format: "identifier == 'item-checkbox' AND value == 'checkmark.circle.fill'")
313 ).firstMatch
314 XCTAssertTrue(completed.waitForExistence(timeout: 3))
315
316 app.menuBars.menuBarItems["Edit"].click()
317 app.menuBars.menuBarItems["Edit"].menus.menuItems["Clear Completed"].click()
318
319 XCTAssertTrue(
320 emptyStateLabel.waitForExistence(timeout: 3),
321 "Empty state should reappear after clearing the only completed item"
322 )
323 }
324
325
326
327 func testShiftDownExtendsSelection() {
328 createItem("Alpha")
329 createItem("Bravo")
330 createItem("Charlie")
331 navigateToItem(at: 0)
332 app.typeKey(.downArrow, modifierFlags: .shift)
333 app.typeKey(.downArrow, modifierFlags: .shift)
334
335 app.typeKey(.delete, modifierFlags: [])
336 XCTAssertTrue(
337 emptyStateLabel.waitForExistence(timeout: 2),
338 "All three items should be deleted after Shift+Down range select"
339 )
340 }
341
342 func testShiftUpContractsSelection() {
343 createItem("Alpha")
344 createItem("Bravo")
345 createItem("Charlie")
346 navigateToItem(at: 0)
347
348 app.typeKey(.downArrow, modifierFlags: .shift)
349 app.typeKey(.downArrow, modifierFlags: .shift)
350
351 app.typeKey(.upArrow, modifierFlags: .shift)
352 app.typeKey(.upArrow, modifierFlags: .shift)
353
354 app.typeKey(.delete, modifierFlags: [])
355 XCTAssertTrue(itemText("Bravo").waitForExistence(timeout: 2), "Bravo should remain")
356 XCTAssertTrue(itemText("Charlie").exists, "Charlie should remain")
357 XCTAssertFalse(itemText("Alpha").exists, "Alpha should be deleted")
358 }
359
360 func testSelectAllThenShiftDown() {
361 createItem("Alpha")
362 createItem("Bravo")
363 createItem("Charlie")
364 navigateToItem(at: 0)
365
366 app.typeKey("a", modifierFlags: .command)
367
368
369 app.typeKey(.downArrow, modifierFlags: .shift)
370 app.typeKey(.delete, modifierFlags: [])
371 XCTAssertTrue(
372 emptyStateLabel.waitForExistence(timeout: 2),
373 "All items should still be selected after Shift+Down at end"
374 )
375 }
376
377
378
379 func testCmdClickDeselectsFromRange() {
380 createItem("Alpha")
381 createItem("Bravo")
382 createItem("Charlie")
383 navigateToItem(at: 0)
384
385 app.typeKey(.downArrow, modifierFlags: .shift)
386 app.typeKey(.downArrow, modifierFlags: .shift)
387
388 cmdClickRow(withText: "Bravo")
389 app.typeKey(.delete, modifierFlags: [])
390
391 XCTAssertTrue(
392 itemText("Bravo").waitForExistence(timeout: 2),
393 "Bravo should remain (was deselected by Cmd+Click)"
394 )
395 XCTAssertFalse(itemText("Alpha").exists, "Alpha should be deleted")
396 XCTAssertFalse(itemText("Charlie").exists, "Charlie should be deleted")
397 }
398
399 func testCmdClickAddsToSelection() {
400 createItem("Alpha")
401 createItem("Bravo")
402 createItem("Charlie")
403 navigateToItem(at: 0)
404
405 cmdClickRow(withText: "Charlie")
406 app.typeKey(.delete, modifierFlags: [])
407
408 XCTAssertTrue(
409 itemText("Bravo").waitForExistence(timeout: 2),
410 "Bravo should remain (was not selected)"
411 )
412 XCTAssertFalse(itemText("Alpha").exists, "Alpha should be deleted")
413 XCTAssertFalse(itemText("Charlie").exists, "Charlie should be deleted")
414 }
415
416 func testShiftUpAfterCmdClickDeselect() {
417 createItem("Delta")
418 createItem("Echo")
419 createItem("Foxtrot")
420 createItem("Golf")
421 navigateToItem(at: 0)
422
423 app.typeKey(.downArrow, modifierFlags: .shift)
424 app.typeKey(.downArrow, modifierFlags: .shift)
425 app.typeKey(.downArrow, modifierFlags: .shift)
426
427 cmdClickRow(withText: "Echo")
428
429 app.typeKey(.upArrow, modifierFlags: .shift)
430 app.typeKey(.delete, modifierFlags: [])
431
432 XCTAssertTrue(itemText("Echo").waitForExistence(timeout: 2), "Echo should remain")
433 XCTAssertTrue(itemText("Golf").exists, "Golf should remain")
434 XCTAssertFalse(itemText("Delta").exists, "Delta should be deleted")
435 XCTAssertFalse(itemText("Foxtrot").exists, "Foxtrot should be deleted")
436 }
437
438 func testSelectAllAfterCmdClick() {
439 createItem("Alpha")
440 createItem("Bravo")
441 createItem("Charlie")
442 navigateToItem(at: 0)
443 app.typeKey(.downArrow, modifierFlags: .shift)
444 app.typeKey(.downArrow, modifierFlags: .shift)
445
446 cmdClickRow(withText: "Bravo")
447
448 app.typeKey("a", modifierFlags: .command)
449 app.typeKey(.delete, modifierFlags: [])
450
451 XCTAssertTrue(
452 emptyStateLabel.waitForExistence(timeout: 2),
453 "All items should be deleted after Select All"
454 )
455 }
456}