listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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 }