← All repositories

listless

A simple list app for Apple platforms

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


Tests/UI/ListlessMacScreenshots.swift (5776B)

1import CoreGraphics
2import XCTest
3
4@MainActor
5final 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}