ListlessMacScreenshots.swift (5776B)
1 import CoreGraphics 2 import XCTest 3 4 @MainActor 5 final class ListlessMacScreenshots: XCTestCase { 6 var app: XCUIApplication! 7 8 override func tearDownWithError() throws { 9 app.terminate() 10 } 11 12 // MARK: - Helpers 13 14 private func launchApp(_ additionalArgs: [String] = []) { 15 app = XCUIApplication() 16 let appearance = additionalArgs.contains("SCREENSHOT_DARK") ? "SCREENSHOT_DARK" : "SCREENSHOT_LIGHT" 17 app.launchArguments = ["UI_TESTING", appearance] + additionalArgs.filter { $0 != "SCREENSHOT_DARK" } 18 app.launch() 19 } 20 21 var draftTextField: XCUIElement { 22 app.textFields["draft-row-append"] 23 } 24 25 func itemText(_ title: String) -> XCUIElement { 26 app.textFields.matching( 27 NSPredicate(format: "identifier BEGINSWITH 'item-text-' AND value == %@", title) 28 ).firstMatch 29 } 30 31 func createItem(_ title: String) { 32 let textField = draftTextField 33 if !textField.exists { 34 app.typeKey("n", modifierFlags: .command) 35 if !textField.waitForExistence(timeout: 2) { 36 XCTFail("Draft text field should appear after Cmd+N") 37 return 38 } 39 } 40 textField.click() 41 textField.typeText(title) 42 textField.typeKey(.return, modifierFlags: []) 43 } 44 45 func itemCheckbox(at index: Int) -> XCUIElement { 46 app.buttons.matching(identifier: "item-checkbox").element(boundBy: index) 47 } 48 49 func navigateToItem(at index: Int) { 50 app.typeKey(.escape, modifierFlags: []) 51 for _ in 0...index { 52 app.typeKey(.downArrow, modifierFlags: []) 53 } 54 } 55 56 func cmdClickRow(withText title: String) { 57 let textField = itemText(title) 58 XCTAssertTrue(textField.waitForExistence(timeout: 2)) 59 let coord = textField.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0.5)) 60 .withOffset(CGVector(dx: -40, dy: 0)) 61 let point = coord.screenPoint 62 63 let src = CGEventSource(stateID: .combinedSessionState) 64 65 let mouseDown = CGEvent( 66 mouseEventSource: src, mouseType: .leftMouseDown, 67 mouseCursorPosition: point, mouseButton: .left 68 )! 69 mouseDown.flags = .maskCommand 70 mouseDown.post(tap: .cgSessionEventTap) 71 usleep(50_000) 72 73 let mouseUp = CGEvent( 74 mouseEventSource: src, mouseType: .leftMouseUp, 75 mouseCursorPosition: point, mouseButton: .left 76 )! 77 mouseUp.flags = .maskCommand 78 mouseUp.post(tap: .cgSessionEventTap) 79 usleep(200_000) 80 } 81 82 func saveScreenshot(name: String) { 83 let windowList = CGWindowListCopyWindowInfo( 84 [.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID 85 ) as? [[String: Any]] ?? [] 86 87 guard let windowID = windowList.first(where: { 88 ($0[kCGWindowOwnerName as String] as? String) == "Listless" 89 && ($0[kCGWindowLayer as String] as? Int) == 0 90 })?[kCGWindowNumber as String] as? Int else { 91 XCTFail("Could not find Listless window") 92 return 93 } 94 95 let tmpFile = NSTemporaryDirectory() + "\(name).png" 96 let process = Process() 97 process.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") 98 process.arguments = ["-l\(windowID)", tmpFile] 99 try? process.run() 100 process.waitUntilExit() 101 102 guard let data = try? Data(contentsOf: URL(fileURLWithPath: tmpFile)) else { 103 XCTFail("screencapture failed to write \(tmpFile)") 104 return 105 } 106 107 let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.png") 108 attachment.name = name 109 attachment.lifetime = .keepAlways 110 add(attachment) 111 112 try? FileManager.default.removeItem(atPath: tmpFile) 113 } 114 115 // MARK: - Screenshots 116 117 /// Four items with draft row focused: three active, one empty draft row, 118 /// one completed at the bottom. 119 func testScreenshot01_ItemsWithEditing() throws { 120 launchApp() 121 122 createItem("Add items on your iPhone") 123 createItem("Edit items on your iPad") 124 createItem("Reorder items on your Mac") 125 createItem("Complete items on your Watch") 126 127 app.typeKey(.escape, modifierFlags: []) 128 usleep(500_000) 129 130 // Complete the last item 131 let checkbox = itemCheckbox(at: 3) 132 XCTAssertTrue(checkbox.waitForExistence(timeout: 2)) 133 checkbox.click() 134 usleep(500_000) 135 136 // Create an empty draft row 137 app.typeKey("n", modifierFlags: .command) 138 XCTAssertTrue(draftTextField.waitForExistence(timeout: 2)) 139 usleep(300_000) 140 141 saveScreenshot(name: "01-items") 142 } 143 144 /// Single item in dark mode. 145 func testScreenshot02_DarkMode() throws { 146 launchApp(["SCREENSHOT_DARK"]) 147 148 createItem("Ask Alfred about utility belt") 149 150 usleep(500_000) 151 152 saveScreenshot(name: "02-click-to-create") 153 } 154 155 /// Multiple items with discontinuous selection via Cmd+Click. 156 func testScreenshot03_MultipleSelection() throws { 157 launchApp() 158 159 createItem("Write shell script") 160 createItem("Rewrite shell script") 161 createItem("Question life choices") 162 createItem("Cry tragically") 163 createItem("Achieve catharsis") 164 165 app.typeKey(.escape, modifierFlags: []) 166 usleep(500_000) 167 168 // Navigate to first item then select it 169 navigateToItem(at: 0) 170 usleep(200_000) 171 172 // Cmd+Click to add non-adjacent items to selection 173 cmdClickRow(withText: "Question life choices") 174 cmdClickRow(withText: "Achieve catharsis") 175 usleep(300_000) 176 177 saveScreenshot(name: "03-selection") 178 } 179 }