Skip to content

Commit 98d9f5a

Browse files
committed
Stabilize project navigator UI tests
1 parent cec6287 commit 98d9f5a

7 files changed

Lines changed: 175 additions & 32 deletions

File tree

CodeEdit/AppDelegate.swift

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
3838
}
3939

4040
for index in 0..<CommandLine.arguments.count {
41+
#if DEBUG
42+
if CommandLine.arguments[index] == "--codeedit-uitest-open-temp-workspace"
43+
&& (index + 1) < CommandLine.arguments.count {
44+
let url = self.prepareUITestWorkspace(id: CommandLine.arguments[index+1])
45+
self.openWorkspace(at: url)
46+
needToHandleOpen = false
47+
continue
48+
}
49+
#endif
50+
4151
if CommandLine.arguments[index] == "--open" && (index + 1) < CommandLine.arguments.count {
4252
let path = CommandLine.arguments[index+1]
4353
let url = URL(fileURLWithPath: path)
4454

45-
CodeEditDocumentController.shared.reopenDocument(
46-
for: url,
47-
withContentsOf: url,
48-
display: true
49-
) { document, _, _ in
50-
document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName()
51-
}
52-
55+
self.openWorkspace(at: url)
5356
needToHandleOpen = false
5457
}
5558
}
@@ -60,6 +63,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
6063
}
6164
}
6265

66+
private func openWorkspace(at url: URL) {
67+
CodeEditDocumentController.shared.reopenDocument(
68+
for: url,
69+
withContentsOf: url,
70+
display: true
71+
) { document, _, _ in
72+
document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName()
73+
}
74+
}
75+
76+
#if DEBUG
77+
private func prepareUITestWorkspace(id: String) -> URL {
78+
let baseURL = FileManager.default.temporaryDirectory
79+
.appending(path: "CodeEditUITests")
80+
let url = baseURL.appending(path: id)
81+
82+
do {
83+
if FileManager.default.fileExists(atPath: baseURL.path(percentEncoded: false)) {
84+
try FileManager.default.removeItem(at: baseURL)
85+
}
86+
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
87+
} catch {
88+
logger.error("Failed to create UI test workspace: \(error.localizedDescription, privacy: .public)")
89+
}
90+
91+
return url
92+
}
93+
#endif
94+
6395
func applicationWillTerminate(_ aNotification: Notification) {
6496

6597
}

CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,13 @@ struct ProjectNavigatorToolbarBottom: View {
132132
alert.runModal()
133133
}
134134
}
135-
} label: {}
136-
.background {
135+
} label: {
137136
Image(systemName: "plus")
138137
.accessibilityHidden(true)
139138
}
140139
.menuStyle(.borderlessButton)
141140
.menuIndicator(.hidden)
142-
.frame(maxWidth: 18, alignment: .center)
141+
.frame(width: 18, alignment: .center)
143142
.opacity(activeState == .inactive ? 0.45 : 1)
144143
.accessibilityLabel("Add Folder or File")
145144
.accessibilityIdentifier("addButton")

CodeEditUITests/App.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ enum App {
2424
return (application, tempDirURL)
2525
}
2626

27+
// Launches CodeEdit with an app-writable directory that CodeEdit creates before opening.
28+
static func launchWithAppWritableTempDir() -> (XCUIApplication, String) {
29+
let tempDirID = appWritableTempProjectID()
30+
let application = XCUIApplication()
31+
application.launchArguments = [
32+
"-ApplePersistenceIgnoreState",
33+
"YES",
34+
"--codeedit-uitest-open-temp-workspace",
35+
tempDirID
36+
]
37+
application.launch()
38+
return (application, tempDirID)
39+
}
40+
2741
static func launch() -> XCUIApplication {
2842
let application = XCUIApplication()
2943
application.launchArguments = ["-ApplePersistenceIgnoreState", "YES"]

CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorFileManagementUITests.swift

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase {
1717
override func setUp() async throws {
1818
// MainActor required for async compatibility which is required to make this method throwing
1919
try await MainActor.run {
20-
(app, path) = try App.launchWithTempDir()
20+
if name.contains("testCreateNewFiles") {
21+
(app, path) = App.launchWithAppWritableTempDir()
22+
} else {
23+
(app, path) = try App.launchWithTempDir()
24+
}
2125

2226
window = Query.getWindow(app)
2327
XCTAssertTrue(window.exists, "Window not found")
@@ -81,29 +85,39 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase {
8185
func testCreateNewFiles() throws {
8286
// Add a few files with the navigator button
8387
for idx in 0..<5 {
84-
let addButton = window.popUpButtons["addButton"]
88+
let previousRowCount = Query.Navigator.getRows(navigator).count
89+
let addButton = Query.Navigator.getAddButton(window)
90+
XCTAssertTrue(addButton.waitForExistence(timeout: 2.0), "Add button not found")
8591
addButton.click()
86-
let addMenu = addButton.menus.firstMatch
87-
addMenu.menuItems["Add File"].click()
8892

89-
let selectedRows = Query.Navigator.getSelectedRows(navigator)
90-
guard selectedRows.firstMatch.waitForExistence(timeout: 0.5) else {
91-
XCTFail("No new selected rows appeared")
92-
return
93-
}
93+
let addFileMenuItem = addButton.menuItems["Add File"]
94+
XCTAssertTrue(addFileMenuItem.waitForExistence(timeout: 2.0), "Add File menu item not found")
95+
addFileMenuItem.click()
9496

9597
let title = idx > 0 ? "untitled\(idx)" : "untitled"
98+
XCTAssertTrue(
99+
Query.Navigator.waitForRowCount(navigator, greaterThan: previousRowCount, timeout: 5.0),
100+
"No new navigator row appeared after adding \(title)"
101+
)
102+
103+
guard let newFileRow = Query.Navigator.waitForProjectNavigatorRow(
104+
fileTitle: title,
105+
navigator,
106+
timeout: 5.0
107+
) else {
108+
XCTFail("\(title) did not appear in the navigator")
109+
return
110+
}
96111

97-
let newFileRow = selectedRows.firstMatch
98112
XCTAssertEqual(newFileRow.descendants(matching: .textField).firstMatch.value as? String, title)
99113

100114
let tabBar = Query.Window.getTabBar(window)
101-
XCTAssertTrue(tabBar.exists)
102-
let readmeTab = Query.TabBar.getTab(labeled: title, tabBar)
103-
XCTAssertTrue(readmeTab.exists)
115+
XCTAssertTrue(tabBar.waitForExistence(timeout: 2.0))
116+
let newFileTab = Query.TabBar.getTab(labeled: title, tabBar)
117+
XCTAssertTrue(newFileTab.waitForExistence(timeout: 2.0))
104118

105119
let newFileEditor = Query.Window.getFirstEditor(window)
106-
XCTAssertTrue(newFileEditor.exists)
120+
XCTAssertTrue(newFileEditor.waitForExistence(timeout: 2.0))
107121
XCTAssertNotNil(newFileEditor.value as? String)
108122
}
109123
}

CodeEditUITests/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorUITests.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ final class ProjectNavigatorUITests: XCTestCase {
2727

2828
// Open the README.md
2929
let readmeRow = Query.Navigator.getProjectNavigatorRow(fileTitle: "README.md", navigator)
30+
XCTAssertTrue(readmeRow.waitForExistence(timeout: 5.0))
3031
XCTAssertFalse(Query.Navigator.rowContainsDisclosureIndicator(readmeRow), "File has disclosure indicator")
3132
readmeRow.click()
3233

@@ -42,19 +43,27 @@ final class ProjectNavigatorUITests: XCTestCase {
4243
let rowCount = navigator.descendants(matching: .outlineRow).count
4344

4445
// Open a folder
45-
let codeEditFolderRow = Query.Navigator.getProjectNavigatorRow(fileTitle: "CodeEdit", index: 1, navigator)
46-
XCTAssertTrue(codeEditFolderRow.exists)
46+
let codeEditFolderRow = Query.Navigator.getLastProjectNavigatorRow(fileTitle: "CodeEdit", navigator)
47+
XCTAssertTrue(codeEditFolderRow.waitForExistence(timeout: 5.0))
48+
let folderDisclosureIndicator = Query.Navigator.disclosureIndicatorForRow(codeEditFolderRow)
4749
XCTAssertTrue(
48-
Query.Navigator.rowContainsDisclosureIndicator(codeEditFolderRow),
50+
folderDisclosureIndicator.waitForExistence(timeout: 2.0),
4951
"Folder doesn't have disclosure indicator"
5052
)
51-
let folderDisclosureIndicator = Query.Navigator.disclosureIndicatorForRow(codeEditFolderRow)
5253
folderDisclosureIndicator.click()
5354

55+
XCTAssertTrue(
56+
Query.Navigator.waitForRowCount(navigator, greaterThan: rowCount, timeout: 2.0),
57+
"No new rows were loaded after opening the folder"
58+
)
5459
let newRowCount = navigator.descendants(matching: .outlineRow).count
5560
XCTAssertTrue(newRowCount > rowCount, "No new rows were loaded after opening the folder")
5661

5762
folderDisclosureIndicator.click()
63+
XCTAssertTrue(
64+
Query.Navigator.waitForRowCount(navigator, equalTo: rowCount, timeout: 2.0),
65+
"Rows were not hidden after closing a folder"
66+
)
5867
let finalRowCount = navigator.descendants(matching: .outlineRow).count
5968
XCTAssertTrue(newRowCount > finalRowCount, "Rows were not hidden after closing a folder")
6069
XCTAssertEqual(rowCount, finalRowCount, "Different Number of rows loaded")

CodeEditUITests/ProjectPath.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ func tempProjectPath() throws -> String {
3636
return path.path(percentEncoded: false)
3737
}
3838

39+
func appWritableTempProjectID() -> String {
40+
makeTempID()
41+
}
42+
3943
func cleanUpTempProjectPaths() throws {
4044
let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests")
4145
try FileManager.default.removeItem(at: baseDir)

CodeEditUITests/Query.swift

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,94 @@ enum Query {
5656
navigator.descendants(matching: .outlineRow)
5757
}
5858

59+
static func getAddButton(_ window: XCUIElement) -> XCUIElement {
60+
if window.buttons["addButton"].exists {
61+
return window.buttons["addButton"]
62+
}
63+
64+
if window.popUpButtons["addButton"].exists {
65+
return window.popUpButtons["addButton"]
66+
}
67+
68+
return window.descendants(matching: .any).matching(identifier: "addButton").firstMatch
69+
}
70+
5971
static func getSelectedRows(_ navigator: XCUIElement) -> XCUIElementQuery {
6072
getRows(navigator).matching(NSPredicate(format: "selected = true"))
6173
}
6274

63-
static func getProjectNavigatorRow(fileTitle: String, index: Int = 0, _ navigator: XCUIElement) -> XCUIElement {
64-
return getRows(navigator)
75+
static func getProjectNavigatorRows(fileTitle: String, _ navigator: XCUIElement) -> XCUIElementQuery {
76+
getRows(navigator)
6577
.containing(.textField, identifier: "ProjectNavigatorTableViewCell-\(fileTitle)")
78+
}
79+
80+
static func getProjectNavigatorRow(fileTitle: String, index: Int = 0, _ navigator: XCUIElement) -> XCUIElement {
81+
return getProjectNavigatorRows(fileTitle: fileTitle, navigator)
6682
.element(boundBy: index)
6783
}
6884

85+
static func waitForProjectNavigatorRow(
86+
fileTitle: String,
87+
index: Int = 0,
88+
_ navigator: XCUIElement,
89+
timeout: TimeInterval
90+
) -> XCUIElement? {
91+
let deadline = Date().addingTimeInterval(timeout)
92+
while Date() < deadline {
93+
let row = getProjectNavigatorRow(fileTitle: fileTitle, index: index, navigator)
94+
if row.exists {
95+
return row
96+
}
97+
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
98+
}
99+
100+
let row = getProjectNavigatorRow(fileTitle: fileTitle, index: index, navigator)
101+
return row.exists ? row : nil
102+
}
103+
104+
static func getLastProjectNavigatorRow(fileTitle: String, _ navigator: XCUIElement) -> XCUIElement {
105+
let matchingRows = getProjectNavigatorRows(fileTitle: fileTitle, navigator)
106+
return matchingRows.element(boundBy: max(matchingRows.count - 1, 0))
107+
}
108+
69109
static func disclosureIndicatorForRow(_ row: XCUIElement) -> XCUIElement {
70-
row.descendants(matching: .disclosureTriangle).element
110+
row.descendants(matching: .disclosureTriangle).firstMatch
71111
}
72112

73113
static func rowContainsDisclosureIndicator(_ row: XCUIElement) -> Bool {
74114
disclosureIndicatorForRow(row).exists
75115
}
116+
117+
static func waitForRowCount(
118+
_ navigator: XCUIElement,
119+
greaterThan rowCount: Int,
120+
timeout: TimeInterval
121+
) -> Bool {
122+
waitForRowCount(navigator, timeout: timeout) { $0 > rowCount }
123+
}
124+
125+
static func waitForRowCount(
126+
_ navigator: XCUIElement,
127+
equalTo rowCount: Int,
128+
timeout: TimeInterval
129+
) -> Bool {
130+
waitForRowCount(navigator, timeout: timeout) { $0 == rowCount }
131+
}
132+
133+
private static func waitForRowCount(
134+
_ navigator: XCUIElement,
135+
timeout: TimeInterval,
136+
predicate: (Int) -> Bool
137+
) -> Bool {
138+
let deadline = Date().addingTimeInterval(timeout)
139+
while Date() < deadline {
140+
if predicate(getRows(navigator).count) {
141+
return true
142+
}
143+
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
144+
}
145+
return predicate(getRows(navigator).count)
146+
}
76147
}
77148

78149
enum TabBar {

0 commit comments

Comments
 (0)