Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions CodeEdit/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}

for index in 0..<CommandLine.arguments.count {
#if DEBUG
if CommandLine.arguments[index] == "--codeedit-uitest-open-temp-workspace"
&& (index + 1) < CommandLine.arguments.count {
let url = self.prepareUITestWorkspace(id: CommandLine.arguments[index+1])
self.openWorkspace(at: url)
needToHandleOpen = false
continue
}
#endif

if CommandLine.arguments[index] == "--open" && (index + 1) < CommandLine.arguments.count {
let path = CommandLine.arguments[index+1]
let url = URL(fileURLWithPath: path)

CodeEditDocumentController.shared.reopenDocument(
for: url,
withContentsOf: url,
display: true
) { document, _, _ in
document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName()
}

self.openWorkspace(at: url)
needToHandleOpen = false
}
}
Expand All @@ -60,6 +63,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
}

private func openWorkspace(at url: URL) {
CodeEditDocumentController.shared.reopenDocument(
for: url,
withContentsOf: url,
display: true
) { document, _, _ in
document?.windowControllers.first?.synchronizeWindowTitleWithDocumentName()
}
}

#if DEBUG
private func prepareUITestWorkspace(id: String) -> URL {
let baseURL = FileManager.default.temporaryDirectory
.appending(path: "CodeEditUITests")
let url = baseURL.appending(path: id)

do {
if FileManager.default.fileExists(atPath: url.path(percentEncoded: false)) {
try FileManager.default.removeItem(at: url)
}
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} catch {
let path = url.path(percentEncoded: false)
let message = "Failed to create UI test workspace at \(path): \(error.localizedDescription)"
logger.error("\(message, privacy: .public)")
fatalError(message)
}

return url
}
#endif

func applicationWillTerminate(_ aNotification: Notification) {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,13 @@ struct ProjectNavigatorToolbarBottom: View {
alert.runModal()
}
}
} label: {}
.background {
} label: {
Image(systemName: "plus")
.accessibilityHidden(true)
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.frame(maxWidth: 18, alignment: .center)
.frame(width: 18, alignment: .center)
.opacity(activeState == .inactive ? 0.45 : 1)
.accessibilityLabel("Add Folder or File")
.accessibilityIdentifier("addButton")
Expand Down
14 changes: 14 additions & 0 deletions CodeEditUITests/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ enum App {
return (application, tempDirURL)
}

// Launches CodeEdit with an app-writable directory that CodeEdit creates before opening.
static func launchWithAppWritableTempDir() -> (XCUIApplication, String) {
let tempDirID = appWritableTempProjectID()
let application = XCUIApplication()
application.launchArguments = [
"-ApplePersistenceIgnoreState",
"YES",
"--codeedit-uitest-open-temp-workspace",
tempDirID
]
application.launch()
return (application, appWritableTempProjectPath(id: tempDirID))
}

static func launch() -> XCUIApplication {
let application = XCUIApplication()
application.launchArguments = ["-ApplePersistenceIgnoreState", "YES"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase {
override func setUp() async throws {
// MainActor required for async compatibility which is required to make this method throwing
try await MainActor.run {
(app, path) = try App.launchWithTempDir()
if name.contains("testCreateNewFiles") {
(app, path) = App.launchWithAppWritableTempDir()
} else {
(app, path) = try App.launchWithTempDir()
}

window = Query.getWindow(app)
XCTAssertTrue(window.exists, "Window not found")
Expand All @@ -26,6 +30,15 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase {
navigator = Query.Window.getProjectNavigator(window)
XCTAssertTrue(navigator.exists, "Navigator not found")
XCTAssertEqual(Query.Navigator.getRows(navigator).count, 1, "Found more than just the root file.")

if name.contains("testCreateNewFiles") {
var isDirectory: ObjCBool = false
XCTAssertTrue(
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory),
"App-writable temp directory was not created at \(path ?? "")"
)
XCTAssertTrue(isDirectory.boolValue, "App-writable temp project path is not a directory")
}
}
}

Expand Down Expand Up @@ -81,29 +94,39 @@ final class ProjectNavigatorFileManagementUITests: XCTestCase {
func testCreateNewFiles() throws {
// Add a few files with the navigator button
for idx in 0..<5 {
let addButton = window.popUpButtons["addButton"]
let previousRowCount = Query.Navigator.getRows(navigator).count
let addButton = Query.Navigator.getAddButton(window)
XCTAssertTrue(addButton.waitForExistence(timeout: 2.0), "Add button not found")
addButton.click()
let addMenu = addButton.menus.firstMatch
addMenu.menuItems["Add File"].click()

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

let title = idx > 0 ? "untitled\(idx)" : "untitled"
XCTAssertTrue(
Query.Navigator.waitForRowCount(navigator, greaterThan: previousRowCount, timeout: 5.0),
"No new navigator row appeared after adding \(title)"
)

guard let newFileRow = Query.Navigator.waitForProjectNavigatorRow(
fileTitle: title,
navigator,
timeout: 5.0
) else {
XCTFail("\(title) did not appear in the navigator")
return
}

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

let tabBar = Query.Window.getTabBar(window)
XCTAssertTrue(tabBar.exists)
let readmeTab = Query.TabBar.getTab(labeled: title, tabBar)
XCTAssertTrue(readmeTab.exists)
XCTAssertTrue(tabBar.waitForExistence(timeout: 2.0))
let newFileTab = Query.TabBar.getTab(labeled: title, tabBar)
XCTAssertTrue(newFileTab.waitForExistence(timeout: 2.0))

let newFileEditor = Query.Window.getFirstEditor(window)
XCTAssertTrue(newFileEditor.exists)
XCTAssertTrue(newFileEditor.waitForExistence(timeout: 2.0))
XCTAssertNotNil(newFileEditor.value as? String)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class ProjectNavigatorUITests: XCTestCase {

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

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

// Open a folder
let codeEditFolderRow = Query.Navigator.getProjectNavigatorRow(fileTitle: "CodeEdit", index: 1, navigator)
XCTAssertTrue(codeEditFolderRow.exists)
let codeEditFolderRow = Query.Navigator.getLastProjectNavigatorRow(fileTitle: "CodeEdit", navigator)
XCTAssertTrue(codeEditFolderRow.waitForExistence(timeout: 5.0))
let folderDisclosureIndicator = Query.Navigator.disclosureIndicatorForRow(codeEditFolderRow)
XCTAssertTrue(
Query.Navigator.rowContainsDisclosureIndicator(codeEditFolderRow),
folderDisclosureIndicator.waitForExistence(timeout: 2.0),
"Folder doesn't have disclosure indicator"
)
let folderDisclosureIndicator = Query.Navigator.disclosureIndicatorForRow(codeEditFolderRow)
folderDisclosureIndicator.click()

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

folderDisclosureIndicator.click()
XCTAssertTrue(
Query.Navigator.waitForRowCount(navigator, equalTo: rowCount, timeout: 2.0),
"Rows were not hidden after closing a folder"
)
let finalRowCount = navigator.descendants(matching: .outlineRow).count
XCTAssertTrue(newRowCount > finalRowCount, "Rows were not hidden after closing a folder")
XCTAssertEqual(rowCount, finalRowCount, "Different Number of rows loaded")
Expand Down
72 changes: 67 additions & 5 deletions CodeEditUITests/ProjectPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ func projectPath() -> String {
}

private var tempProjectPathIds = Set<String>()
private let codeEditAppBundleID = "app.codeedit.CodeEdit"

private func testRunnerTempProjectURL(id: String) -> URL {
FileManager.default.temporaryDirectory
.appending(path: "CodeEditUITests")
.appending(path: id)
}

private func userHomeDirectoryOutsideSandbox() -> URL {
let homePath = NSHomeDirectoryForUser(NSUserName()) ?? NSHomeDirectory()
// UI test runners are sandboxed too; strip that container before addressing the app's container.
if let containerRange = homePath.range(of: "/Library/Containers/") {
return URL(fileURLWithPath: String(homePath[..<containerRange.lowerBound]), isDirectory: true)
}
return URL(fileURLWithPath: homePath, isDirectory: true)
}

private func appWritableTempProjectURL(id: String) -> URL {
// CodeEdit is sandboxed, so app-created temporary workspaces live in the app container.
userHomeDirectoryOutsideSandbox()
.appending(path: "Library")
.appending(path: "Containers")
.appending(path: codeEditAppBundleID)
.appending(path: "Data")
.appending(path: "tmp")
.appending(path: "CodeEditUITests")
.appending(path: id)
}

private func makeTempID() -> String {
let id = String((0..<10).map { _ in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-".randomElement()! })
Expand All @@ -29,15 +57,49 @@ private func makeTempID() -> String {
}

func tempProjectPath() throws -> String {
let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests")
let id = makeTempID()
let path = baseDir.appending(path: id)
let path = testRunnerTempProjectURL(id: id)
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
return path.path(percentEncoded: false)
}

func appWritableTempProjectID() -> String {
makeTempID()
}

func appWritableTempProjectPath(id: String) -> String {
appWritableTempProjectURL(id: id).path(percentEncoded: false)
}

func cleanUpTempProjectPaths() throws {
let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests")
try FileManager.default.removeItem(at: baseDir)
tempProjectPathIds.removeAll()
let fileManager = FileManager.default
var cleanupError: Error?
var remainingIDs = Set<String>()

for id in tempProjectPathIds {
let paths = [
testRunnerTempProjectURL(id: id),
appWritableTempProjectURL(id: id)
]
var didFailCleanup = false

for path in paths where fileManager.fileExists(atPath: path.path(percentEncoded: false)) {
do {
try fileManager.removeItem(at: path)
} catch {
cleanupError = cleanupError ?? error
didFailCleanup = true
}
}

if didFailCleanup {
remainingIDs.insert(id)
}
}

tempProjectPathIds = remainingIDs

if let cleanupError {
throw cleanupError
}
}
Loading