From d207d44b1a3aa16a4202216b754574f71a7e6206 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:19:23 -0600 Subject: [PATCH 01/12] Fix Bug (no tests yet) --- CodeEdit.xcodeproj/project.pbxproj | 8 ++++---- .../ActivityViewer/Tasks/TaskDropDownView.swift | 6 ++++-- .../ActivityViewer/Tasks/TasksPopoverMenuItem.swift | 7 ++++--- .../CodeEditUI/Views/InstantPopoverModifier.swift | 11 +++++++++-- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index bf5c71f978..35640ea6fa 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -2705,14 +2705,14 @@ 618725A22C29EFE200987354 /* Tasks */ = { isa = PBXGroup; children = ( + B69D3EE22C5F536B005CF43A /* ActiveTaskView.swift */, + 618725A52C29F02500987354 /* DropdownMenuItemStyleModifier.swift */, + 618725A72C29F05500987354 /* OptionMenuItemView.swift */, 618725A02C29EFCC00987354 /* SchemeDropDownView.swift */, 618725AA2C29F2C000987354 /* TaskDropDownView.swift */, - B69D3EE02C5F5357005CF43A /* TaskView.swift */, - B69D3EE22C5F536B005CF43A /* ActiveTaskView.swift */, B69D3EE42C5F54B3005CF43A /* TasksPopoverMenuItem.swift */, + B69D3EE02C5F5357005CF43A /* TaskView.swift */, 618725A32C29F00400987354 /* WorkspaceMenuItemView.swift */, - 618725A72C29F05500987354 /* OptionMenuItemView.swift */, - 618725A52C29F02500987354 /* DropdownMenuItemStyleModifier.swift */, ); path = Tasks; sourceTree = ""; diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift index 2382cbec03..243f1c1db4 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift @@ -38,7 +38,7 @@ struct TaskDropDownView: View { .onHover { hovering in self.isHoveringTasks = hovering } - .instantPopover(isPresented: $isTaskPopOverPresented, arrowEdge: .bottom) { + .instantPopover(isPresented: $isTaskPopOverPresented, arrowEdge: .top) { taskPopoverContent } .onTapGesture { @@ -71,7 +71,9 @@ struct TaskDropDownView: View { VStack(alignment: .leading, spacing: 0) { if !taskManager.availableTasks.isEmpty { ForEach(taskManager.availableTasks, id: \.id) { task in - TasksPopoverMenuItem(taskManager: taskManager, task: task) + TasksPopoverMenuItem(taskManager: taskManager, task: task) { + isTaskPopOverPresented = false + } } Divider() .padding(.vertical, 5) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift index 0cb3a02c28..cc34a4c42d 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift @@ -7,12 +7,13 @@ import SwiftUI +/// - Note: This view **cannot** use the `dismiss` environment value to dismiss the sheet. It has to negate the boolean +/// value that presented it initially. +/// See ``SwiftUI/View/instantPopover(isPresented:arrowEdge:content:)`` struct TasksPopoverMenuItem: View { - @Environment(\.dismiss) - private var dismiss - @ObservedObject var taskManager: TaskManager var task: CETask + var dismiss: () -> Void var body: some View { HStack(spacing: 5) { diff --git a/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift b/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift index 0c2b86d0d4..c1978b1396 100644 --- a/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift +++ b/CodeEdit/Features/CodeEditUI/Views/InstantPopoverModifier.swift @@ -7,6 +7,9 @@ import SwiftUI +/// See ``SwiftUI/View/instantPopover(isPresented:arrowEdge:content:)`` +/// - Warning: Views presented using this sheet must be dismissed by negating the `isPresented` binding. Using +/// SwiftUI's `dismiss` will likely cause a crash. See [FB16221871](rdar://FB16221871) struct InstantPopoverModifier: ViewModifier { @Binding var isPresented: Bool let arrowEdge: Edge @@ -24,6 +27,9 @@ struct InstantPopoverModifier: ViewModifier { } } +/// See ``SwiftUI/View/instantPopover(isPresented:arrowEdge:content:)`` +/// - Warning: Views presented using this sheet must be dismissed by negating the `isPresented` binding. Using +/// SwiftUI's `dismiss` will likely cause a crash. See [FB16221871](rdar://FB16221871) struct PopoverPresenter: NSViewRepresentable { @Binding var isPresented: Bool let arrowEdge: Edge @@ -32,7 +38,7 @@ struct PopoverPresenter: NSViewRepresentable { func makeNSView(context: Context) -> NSView { NSView() } func updateNSView(_ nsView: NSView, context: Context) { - if isPresented, context.coordinator.popover == nil { + if isPresented && context.coordinator.popover == nil { let popover = NSPopover() popover.animates = false let hostingController = NSHostingController(rootView: contentView) @@ -109,8 +115,9 @@ struct PopoverPresenter: NSViewRepresentable { } extension View { - /// A custom view modifier that presents a popover attached to the view with no animation. + /// - Warning: Views presented using this sheet must be dismissed by negating the `isPresented` binding. Using + /// SwiftUI's `dismiss` will likely cause a crash. See [FB16221871](rdar://FB16221871) /// - Parameters: /// - isPresented: A binding to whether the popover is presented. /// - arrowEdge: The edge of the view that the popover points to. Defaults to `.bottom`. From 20b2b7b86ed349d8041690e5cb0dd222e0f04518 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:55:16 -0600 Subject: [PATCH 02/12] Add Accessibility to Activity Viewer Elements --- .../ActivityViewer/ActivityViewer.swift | 2 + .../CECircularProgressView.swift | 5 ++ .../Notifications/TaskNotificationView.swift | 3 +- .../Tasks/OptionMenuItemView.swift | 6 ++ .../Tasks/SchemeDropDownView.swift | 73 ++++++++++++------- .../Tasks/TaskDropDownView.swift | 8 ++ .../ActivityViewer/Tasks/TaskView.swift | 2 + .../Tasks/TasksPopoverMenuItem.swift | 14 +++- .../Tasks/WorkspaceMenuItemView.swift | 4 +- 9 files changed, 83 insertions(+), 34 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift index ecac0f94ac..5b3e410691 100644 --- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift +++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift @@ -59,5 +59,7 @@ struct ActivityViewer: View { .opacity(0.1) } } + .accessibilityElement(children: .contain) + .accessibilityLabel("Activity Viewer") } } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift b/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift index df8289a83c..e6580f36d0 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/CECircularProgressView.swift @@ -50,6 +50,11 @@ struct CECircularProgressView: View { .font(.caption) } } + .accessibilityElement() + .accessibilityAddTraits(.updatesFrequently) + .accessibilityValue( + progress != nil ? Text(progress!, format: .percent) : Text("working") + ) } } diff --git a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift index d86df90d9b..e8976d81e5 100644 --- a/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift +++ b/CodeEdit/Features/ActivityViewer/Notifications/TaskNotificationView.swift @@ -51,7 +51,8 @@ struct TaskNotificationView: View { .padding(.trailing, 3) .popover(isPresented: $isPresented, arrowEdge: .bottom) { TaskNotificationsDetailView(taskNotificationHandler: taskNotificationHandler) - }.onTapGesture { + } + .onTapGesture { self.isPresented.toggle() } } diff --git a/CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift b/CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift index a00c8ecdb5..49a78560ef 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/OptionMenuItemView.swift @@ -10,6 +10,7 @@ import SwiftUI struct OptionMenuItemView: View { var label: String var action: () -> Void + var body: some View { HStack { Text(label) @@ -22,6 +23,11 @@ struct OptionMenuItemView: View { .onTapGesture { action() } + .accessibilityElement() + .accessibilityAction { + action() + } + .accessibilityLabel(label) } } diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index 80e51d0d6c..c69a1fa064 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -21,15 +21,18 @@ struct SchemeDropDownView: View { workspaceSettingsManager.settings.project.projectName } + /// Resolves the name one step further than `workspaceName`. + var workspaceDisplayName: String { + workspaceName.isEmpty + ? (workspaceFileManager?.workspaceItem.fileName() ?? "No Project found") + : workspaceName + } + var body: some View { HStack(spacing: 6) { Image(systemName: "folder.badge.gearshape") .imageScale(.medium) - Text( - workspaceName.isEmpty - ? (workspaceFileManager?.workspaceItem.fileName() ?? "No Project found") - : workspaceName - ) + Text(workspaceDisplayName) } .font(.subheadline) .padding(.trailing, 11.5) @@ -54,31 +57,18 @@ struct SchemeDropDownView: View { self.isHoveringScheme = hovering }) .instantPopover(isPresented: $isSchemePopOverPresented, arrowEdge: .bottom) { - VStack(alignment: .leading, spacing: 0) { - WorkspaceMenuItemView( - workspaceFileManager: workspaceFileManager, - item: workspaceFileManager?.workspaceItem - ) - Divider() - .padding(.vertical, 5) - Group { - OptionMenuItemView(label: "Add Folder...") { - // TODO: Implment Add Folder - print("NOT IMPLEMENTED") - } - OptionMenuItemView(label: "Workspace Settings...") { - NSApp.sendAction( - #selector(CodeEditWindowController.openWorkspaceSettings(_:)), to: nil, from: nil - ) - } - } - } - .font(.subheadline) - .padding(5) - .frame(minWidth: 215) + popoverContent } .onTapGesture { - self.isSchemePopOverPresented.toggle() + isSchemePopOverPresented.toggle() + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityIdentifier("SchemeDropdown") + .accessibilityValue(workspaceDisplayName) + .accessibilityLabel("Active Scheme") + .accessibilityAction(named: "show menu") { + isSchemePopOverPresented.toggle() } } @@ -97,6 +87,33 @@ struct SchemeDropDownView: View { .font(.system(size: 8, weight: .semibold, design: .default)) .padding(.top, 0.5) } + + @ViewBuilder + var popoverContent: some View { + VStack(alignment: .leading, spacing: 0) { + WorkspaceMenuItemView( + workspaceFileManager: workspaceFileManager, + item: workspaceFileManager?.workspaceItem + ) + Divider() + .padding(.vertical, 5) + Group { + OptionMenuItemView(label: "Add Folder...") { + // TODO: Implment Add Folder + print("NOT IMPLEMENTED") + } + .disabled(true) + OptionMenuItemView(label: "Workspace Settings...") { + NSApp.sendAction( + #selector(CodeEditWindowController.openWorkspaceSettings(_:)), to: nil, from: nil + ) + } + } + } + .font(.subheadline) + .padding(5) + .frame(minWidth: 215) + } } // #Preview { diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift index 243f1c1db4..50237f6711 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift @@ -44,6 +44,14 @@ struct TaskDropDownView: View { .onTapGesture { self.isTaskPopOverPresented.toggle() } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityIdentifier("TaskDropdown") + .accessibilityValue(taskManager.selectedTask?.name ?? "Create Tasks") + .accessibilityLabel("Active Task") + .accessibilityAction(named: "show menu") { + isTaskPopOverPresented = true + } } private var backgroundColor: some View { diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift index 92afa07260..a2c1e3edc2 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskView.swift @@ -27,5 +27,7 @@ struct TaskView: View { .frame(width: 5, height: 5) .padding(.trailing, 2.5) } + .accessibilityElement() + .accessibilityLabel(task.name) } } diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift index cc34a4c42d..2205660b3c 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TasksPopoverMenuItem.swift @@ -23,11 +23,12 @@ struct TasksPopoverMenuItem: View { .padding(.vertical, 4) .padding(.horizontal, 8) .modifier(DropdownMenuItemStyleModifier()) - .onTapGesture { - taskManager.selectedTaskID = task.id - dismiss() - } + .onTapGesture(perform: selectAction) .clipShape(RoundedRectangle(cornerRadius: 5)) + .accessibilityElement() + .accessibilityLabel(task.name) + .accessibilityAction(.default, selectAction) + .accessibilityAddTraits(taskManager.selectedTaskID == task.id ? [.isSelected] : []) } private var selectionIndicator: some View { @@ -53,4 +54,9 @@ struct TasksPopoverMenuItem: View { } } } + + private func selectAction() { + taskManager.selectedTaskID = task.id + dismiss() + } } diff --git a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift index 853bcdc95c..6eaa8f262e 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/WorkspaceMenuItemView.swift @@ -30,8 +30,10 @@ struct WorkspaceMenuItemView: View { .padding(.vertical, 4) .padding(.horizontal, 8) .modifier(DropdownMenuItemStyleModifier()) - .onTapGesture { } + .onTapGesture { } // add accessibility action when this is filled in .clipShape(RoundedRectangle(cornerRadius: 5)) + .accessibilityElement() + .accessibilityLabel(item?.name ?? "") } } From 636f5fff144aaff4d754b6d8f1e32f1211b39284 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:57:36 -0600 Subject: [PATCH 03/12] Add More Accessibility Traits --- .../Features/ActivityViewer/Tasks/SchemeDropDownView.swift | 3 ++- CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index c69a1fa064..c6269a3023 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -67,7 +67,8 @@ struct SchemeDropDownView: View { .accessibilityIdentifier("SchemeDropdown") .accessibilityValue(workspaceDisplayName) .accessibilityLabel("Active Scheme") - .accessibilityAction(named: "show menu") { + .accessibilityHint("Open the active scheme menu") + .accessibilityAction { isSchemePopOverPresented.toggle() } } diff --git a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift index 50237f6711..c3f64b3916 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/TaskDropDownView.swift @@ -49,7 +49,8 @@ struct TaskDropDownView: View { .accessibilityIdentifier("TaskDropdown") .accessibilityValue(taskManager.selectedTask?.name ?? "Create Tasks") .accessibilityLabel("Active Task") - .accessibilityAction(named: "show menu") { + .accessibilityHint("Open the active task menu") + .accessibilityAction { isTaskPopOverPresented = true } } From c6a4af28daaffc2556acee63e1d8084c187f15ac Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:46:50 -0600 Subject: [PATCH 04/12] Add UI Tests --- CodeEdit.xcodeproj/project.pbxproj | 20 +++++++++ .../Views/AddCETaskView.swift | 1 + .../Views/CETaskFormView.swift | 3 ++ .../CEWorkspaceSettingsTaskListView.swift | 1 + .../Views/CEWorkspaceSettingsView.swift | 2 + .../CodeEditWindowControllerExtensions.swift | 1 + CodeEditUITests/App.swift | 11 ++++- .../Tasks/TasksMenuUITests.swift | 41 +++++++++++++++++++ CodeEditUITests/ProjectPath.swift | 24 +++++++++++ 9 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 35640ea6fa..71324988bd 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -357,6 +357,7 @@ 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */; }; 6C05CF9E2CDE8699006AAECD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C05CF9D2CDE8699006AAECD /* CodeEditSourceEditor */; }; 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0617D52BDB4432008C9C42 /* LogStream */; }; + 6C07383B2D284ECA0025CBE3 /* TasksMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C07383A2D284ECA0025CBE3 /* TasksMenuUITests.swift */; }; 6C08249C2C556F7400A0751E /* TerminalCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249B2C556F7400A0751E /* TerminalCache.swift */; }; 6C08249E2C55768400A0751E /* UtilityAreaTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */; }; 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0824A02C5C0C9700A0751E /* SwiftTerm */; }; @@ -1051,6 +1052,7 @@ 66F370332BEE537B00D3B823 /* NonTextFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonTextFileView.swift; sourceTree = ""; }; 6C049A362A49E2DB00D42923 /* DirectoryEventStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryEventStream.swift; sourceTree = ""; }; 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkspaceDocument+Listeners.swift"; sourceTree = ""; }; + 6C07383A2D284ECA0025CBE3 /* TasksMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TasksMenuUITests.swift; sourceTree = ""; }; 6C08249B2C556F7400A0751E /* TerminalCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCache.swift; sourceTree = ""; }; 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminal.swift; sourceTree = ""; }; 6C092ED92A53A58600489202 /* EditorLayout+StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorLayout+StateRestoration.swift"; sourceTree = ""; }; @@ -2838,6 +2840,22 @@ name = "Recovered References"; sourceTree = ""; }; + 6C0738382D284EA20025CBE3 /* ActivityViewer */ = { + isa = PBXGroup; + children = ( + 6C0738392D284EAE0025CBE3 /* Tasks */, + ); + path = ActivityViewer; + sourceTree = ""; + }; + 6C0738392D284EAE0025CBE3 /* Tasks */ = { + isa = PBXGroup; + children = ( + 6C07383A2D284ECA0025CBE3 /* TasksMenuUITests.swift */, + ); + path = Tasks; + sourceTree = ""; + }; 6C092EDC2A53A63E00489202 /* Views */ = { isa = PBXGroup; children = ( @@ -3004,6 +3022,7 @@ 6C96191E2C3F27E3009733CE /* Features */ = { isa = PBXGroup; children = ( + 6C0738382D284EA20025CBE3 /* ActivityViewer */, 6C96191D2C3F27E3009733CE /* NavigatorArea */, ); path = Features; @@ -4581,6 +4600,7 @@ 6CFBA54B2C4E168A00E3A914 /* App.swift in Sources */, 6CFBA54D2C4E16C900E3A914 /* WindowCloseCommandTests.swift in Sources */, 6C9619222C3F27F1009733CE /* Query.swift in Sources */, + 6C07383B2D284ECA0025CBE3 /* TasksMenuUITests.swift in Sources */, 6C9619202C3F27E3009733CE /* ProjectNavigatorUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift index af6fb0be22..2863e90e46 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/AddCETaskView.swift @@ -42,6 +42,7 @@ struct AddCETaskView: View { } .padding() } + .accessibilityIdentifier("AddTaskView") } } diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift index ea41594503..f3f47a3eb2 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CETaskFormView.swift @@ -19,6 +19,7 @@ struct CETaskFormView: View { TextField(text: $task.name) { Text("Name") } + .accessibilityLabel("Task Name") Picker("Target", selection: $task.target) { Text("My Mac") .tag("My Mac") @@ -32,12 +33,14 @@ struct CETaskFormView: View { Text("Docker Compose") .tag("Docker Compose") } + .disabled(true) } Section { TextField(text: $task.command) { Text("Task") } + .accessibilityLabel("Task Command") TextField(text: $task.workingDirectory) { Text("Working Directory") } diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift index d4f3f3efda..331bc2345b 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsTaskListView.swift @@ -15,6 +15,7 @@ struct CEWorkspaceSettingsTaskListView: View { @Binding var selectedTaskID: UUID? @Binding var showAddTaskSheet: Bool + var body: some View { if settings.tasks.isEmpty { Text("No tasks") diff --git a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift index e1284b23a7..682d63b256 100644 --- a/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift +++ b/CodeEdit/Features/CEWorkspaceSettings/Views/CEWorkspaceSettingsView.swift @@ -26,8 +26,10 @@ struct CEWorkspaceSettingsView: View { "Name", text: $workspaceSettingsManager.settings.project.projectName ) + .accessibilityLabel("Workspace Name") } header: { Text("Workspace") + .accessibilityHidden(true) } Section { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 6c487cefdb..9d7ffe4d2d 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -125,6 +125,7 @@ extension CodeEditWindowController { settingsWindow.contentView = NSHostingView(rootView: contentView) settingsWindow.titlebarAppearsTransparent = true settingsWindow.setContentSize(NSSize(width: 515, height: 515)) + settingsWindow.setAccessibilityTitle("Workspace Settings") window.beginSheet(settingsWindow, completionHandler: nil) } diff --git a/CodeEditUITests/App.swift b/CodeEditUITests/App.swift index bd34735cc1..653544da20 100644 --- a/CodeEditUITests/App.swift +++ b/CodeEditUITests/App.swift @@ -10,13 +10,22 @@ import XCTest enum App { static func launchWithCodeEditWorkspace() -> XCUIApplication { let application = XCUIApplication() - application.launchArguments = ["--open", projectPath()] + application.launchArguments = ["-ApplePersistenceIgnoreState", "YES", "--open", projectPath()] + application.launch() + return application + } + + // Launches CodeEdit in a new directory + static func launchWithTempDir() throws -> XCUIApplication { + let application = XCUIApplication() + application.launchArguments = ["-ApplePersistenceIgnoreState", "YES", "--open", try tempProjectPath()] application.launch() return application } static func launch() -> XCUIApplication { let application = XCUIApplication() + application.launchArguments = ["-ApplePersistenceIgnoreState", "YES"] application.launch() return application } diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift new file mode 100644 index 0000000000..0a0db99703 --- /dev/null +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -0,0 +1,41 @@ +// +// ActivityViewerTasksMenuTests.swift +// CodeEditUITests +// +// Created by Khan Winter on 1/3/25. +// + +import XCTest + +final class ActivityViewerTasksMenuTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/CodeEditUITests/ProjectPath.swift b/CodeEditUITests/ProjectPath.swift index 272adc42aa..c3b94e5704 100644 --- a/CodeEditUITests/ProjectPath.swift +++ b/CodeEditUITests/ProjectPath.swift @@ -16,3 +16,27 @@ func projectPath() -> String { .dropFirst() ) } + +private var tempProjectPathIds = Set() + +private func makeTempID() -> String { + let id = String((0..<10).map { _ in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-".randomElement()! }) + if tempProjectPathIds.contains(id) { + return makeTempID() + } + tempProjectPathIds.insert(id) + return id +} + +func tempProjectPath() throws -> String { + let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests") + let id = makeTempID() + let path = baseDir.appending(path: id) + try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) + return path.path(percentEncoded: false) +} + +func cleanUpTempProjectPaths() throws { + let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests") + try FileManager.default.removeItem(at: baseDir) +} From 34558a015a14701ad0cbead03dd08d36b38a7131 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:47:09 -0600 Subject: [PATCH 05/12] UI Tests (got missed in last commit) --- .../Tasks/TasksMenuUITests.swift | 89 ++++++++++++++----- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index 0a0db99703..c21d6317e9 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -8,34 +8,81 @@ import XCTest final class ActivityViewerTasksMenuTests: XCTestCase { + // After all tests in this group + override static func tearDown() { + do { + try cleanUpTempProjectPaths() + } catch { + print("Failed to clean up test temp directories.") + print(error) + } + } - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false + var app: XCUIApplication! + var window: XCUIElement! - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + @MainActor + override func setUp() async throws { + app = try App.launchWithTempDir() + window = Query.getWindow(app) + XCTAssertTrue(window.exists, "Window not found") } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + func testOpenTaskMenu() { + let viewer = window.groups["Activity Viewer"] + XCTAssertNotNil(viewer, "No Activity Viewer") - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() + let taskDropdown = viewer.buttons["Active Task"] + XCTAssertTrue(taskDropdown.exists, "No Task Dropdown") + XCTAssertEqual(taskDropdown.value as? String, "Create Tasks", "Incorrect empty tasks label") - // Use XCTAssert and related functions to verify your tests produce the correct results. + taskDropdown.click() + XCTAssertGreaterThan(app.popovers.count, 0, "Popover didn't show up") } - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + func testNewTask() { + let viewer = window.groups["Activity Viewer"] + let taskDropdown = viewer.buttons["Active Task"] + taskDropdown.click() + let popover = app.popovers.firstMatch + XCTAssertTrue(popover.exists, "Popover did not appear on click") + + let addTaskListOption = popover.buttons["Add Task..."] + XCTAssertTrue(addTaskListOption.exists, "No add task option in dropdown") + addTaskListOption.click() + + let workspaceSettingsWindow = window.sheets["Workspace Settings"] + XCTAssertTrue(workspaceSettingsWindow.exists, "Workspace settings did not appear") + + let addTaskButton = workspaceSettingsWindow.buttons["Add Task..."] + XCTAssertTrue(addTaskButton.exists, "No add task button") + addTaskButton.click() + + // Enter in task information + let newSheet = workspaceSettingsWindow.sheets.firstMatch + XCTAssertTrue(newSheet.exists) + let taskName = newSheet.textFields["Task Name"] + XCTAssertTrue(taskName.exists) + taskName.click() + taskName.typeText("New Test Task") + XCTAssertEqual(taskName.value as? String, "New Test Task", "Name did not enter in") + + let taskCommand = newSheet.textFields["Task Command"] + XCTAssertTrue(taskCommand.exists) + taskCommand.click() + taskCommand.typeText("echo \"Hello World\"") + XCTAssertEqual(taskCommand.value as? String, "echo \"Hello World\"", "Command did not enter in") + + let saveButton = newSheet.buttons["Save"] + XCTAssertTrue(saveButton.exists) + saveButton.click() + + workspaceSettingsWindow.buttons["Done"].click() + XCTAssertFalse(workspaceSettingsWindow.exists, "Workspace Settings should have dismissed") + + // Ensure the new task was added as an option + XCTAssertEqual(taskDropdown.value as? String, "New Test Task") + taskDropdown.click() + XCTAssertTrue(popover.buttons["New Test Task"].exists, "New task was not added to the task list.") } } From f94b6fc2c1a07e93dba0b47e313dc6e7c7de6fc2 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:50:08 -0600 Subject: [PATCH 06/12] Clear Temp IDs --- CodeEditUITests/ProjectPath.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeEditUITests/ProjectPath.swift b/CodeEditUITests/ProjectPath.swift index c3b94e5704..457d7ddbe2 100644 --- a/CodeEditUITests/ProjectPath.swift +++ b/CodeEditUITests/ProjectPath.swift @@ -39,4 +39,5 @@ func tempProjectPath() throws -> String { func cleanUpTempProjectPaths() throws { let baseDir = FileManager.default.temporaryDirectory.appending(path: "CodeEditUITests") try FileManager.default.removeItem(at: baseDir) + tempProjectPathIds.removeAll() } From 7b0f3404346b5cfd657e12cfb407352d7c6cc8ab Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:51:06 -0600 Subject: [PATCH 07/12] Attribute On Same Line --- .../Features/ActivityViewer/Tasks/SchemeDropDownView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift index c6269a3023..d2411ae8cd 100644 --- a/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift +++ b/CodeEdit/Features/ActivityViewer/Tasks/SchemeDropDownView.swift @@ -89,8 +89,7 @@ struct SchemeDropDownView: View { .padding(.top, 0.5) } - @ViewBuilder - var popoverContent: some View { + @ViewBuilder var popoverContent: some View { VStack(alignment: .leading, spacing: 0) { WorkspaceMenuItemView( workspaceFileManager: workspaceFileManager, From dd137b9a74186a23eb3a74c6e978c464e510dd3d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:44:30 -0600 Subject: [PATCH 08/12] Comment Out Failing Test --- .../CEWorkspaceFileManagerTests.swift | 62 ++++++++++--------- .../Tasks/TasksMenuUITests.swift | 2 +- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift index 60f7a3040c..d10111fafa 100644 --- a/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift +++ b/CodeEditTests/Utils/CEWorkspaceFileManager/CEWorkspaceFileManagerTests.swift @@ -63,36 +63,38 @@ final class CEWorkspaceFileManagerUnitTests: XCTestCase { } func testDirectoryChanges() throws { - let client = CEWorkspaceFileManager( - folderUrl: directory, - ignoredFilesAndFolders: [], - sourceControlManager: nil - ) - - let newFile = generateRandomFiles(amount: 1)[0] - let expectation = XCTestExpectation(description: "wait for files") - - let observer = DummyObserver { - let url = client.folderUrl.appending(path: newFile).path - if client.flattenedFileItems[url] != nil { - expectation.fulfill() - } - } - client.addObserver(observer) - - var files = client.flattenedFileItems.map { $0.value.name } - files.append(newFile) - try files.forEach { - let fakeData = Data("fake string".utf8) - let fileUrl = directory - .appendingPathComponent($0) - try fakeData.write(to: fileUrl) - } - - wait(for: [expectation]) - XCTAssertEqual(files.count, client.flattenedFileItems.count - 1) - try FileManager.default.removeItem(at: directory) - client.removeObserver(observer) + // This test is flaky on CI. Right now, the mac runner can take hours to send the file system events that + // this relies on. Commenting out for now to make automated testing feasible. +// let client = CEWorkspaceFileManager( +// folderUrl: directory, +// ignoredFilesAndFolders: [], +// sourceControlManager: nil +// ) +// +// let newFile = generateRandomFiles(amount: 1)[0] +// let expectation = XCTestExpectation(description: "wait for files") +// +// let observer = DummyObserver { +// let url = client.folderUrl.appending(path: newFile).path +// if client.flattenedFileItems[url] != nil { +// expectation.fulfill() +// } +// } +// client.addObserver(observer) +// +// var files = client.flattenedFileItems.map { $0.value.name } +// files.append(newFile) +// try files.forEach { +// let fakeData = Data("fake string".utf8) +// let fileUrl = directory +// .appendingPathComponent($0) +// try fakeData.write(to: fileUrl) +// } +// +// wait(for: [expectation], timeout: 2.0) +// XCTAssertEqual(files.count, client.flattenedFileItems.count - 1) +// try FileManager.default.removeItem(at: directory) +// client.removeObserver(observer) } func generateRandomFiles(amount: Int) -> [String] { diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index c21d6317e9..4151182593 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -28,7 +28,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { XCTAssertTrue(window.exists, "Window not found") } - func testOpenTaskMenu() { + func testTaskMenu() { let viewer = window.groups["Activity Viewer"] XCTAssertNotNil(viewer, "No Activity Viewer") From a647034dd1ddc83926357a8b111e4c2c903d6db8 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:12:10 -0600 Subject: [PATCH 09/12] Wait For Sheets and Windows --- .../ActivityViewer/Tasks/TasksMenuUITests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index 4151182593..283831f7e4 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -33,7 +33,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { XCTAssertNotNil(viewer, "No Activity Viewer") let taskDropdown = viewer.buttons["Active Task"] - XCTAssertTrue(taskDropdown.exists, "No Task Dropdown") + XCTAssertTrue(taskDropdown.waitForExistence(timeout: 2.0), "No Task Dropdown") XCTAssertEqual(taskDropdown.value as? String, "Create Tasks", "Incorrect empty tasks label") taskDropdown.click() @@ -45,14 +45,14 @@ final class ActivityViewerTasksMenuTests: XCTestCase { let taskDropdown = viewer.buttons["Active Task"] taskDropdown.click() let popover = app.popovers.firstMatch - XCTAssertTrue(popover.exists, "Popover did not appear on click") + XCTAssertTrue(popover.waitForExistence(timeout: 2.0), "Popover did not appear on click") let addTaskListOption = popover.buttons["Add Task..."] XCTAssertTrue(addTaskListOption.exists, "No add task option in dropdown") addTaskListOption.click() let workspaceSettingsWindow = window.sheets["Workspace Settings"] - XCTAssertTrue(workspaceSettingsWindow.exists, "Workspace settings did not appear") + XCTAssertTrue(workspaceSettingsWindow.waitForExistence(timeout: 2.0), "Workspace settings did not appear") let addTaskButton = workspaceSettingsWindow.buttons["Add Task..."] XCTAssertTrue(addTaskButton.exists, "No add task button") @@ -60,7 +60,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { // Enter in task information let newSheet = workspaceSettingsWindow.sheets.firstMatch - XCTAssertTrue(newSheet.exists) + XCTAssertTrue(newSheet.waitForExistence(timeout: 2.0), "New task sheet did not appear") let taskName = newSheet.textFields["Task Name"] XCTAssertTrue(taskName.exists) taskName.click() @@ -78,7 +78,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { saveButton.click() workspaceSettingsWindow.buttons["Done"].click() - XCTAssertFalse(workspaceSettingsWindow.exists, "Workspace Settings should have dismissed") + XCTAssertFalse(workspaceSettingsWindow.waitForNonExistence(timeout: 2.0), "Workspace Settings should have dismissed") // Ensure the new task was added as an option XCTAssertEqual(taskDropdown.value as? String, "New Test Task") From a1e051b62f7f2437aaba3173cc4a827146475af7 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:30:31 -0600 Subject: [PATCH 10/12] Lint Error In UI Tests --- .../Features/ActivityViewer/Tasks/TasksMenuUITests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index 283831f7e4..c4ef81710d 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -78,7 +78,10 @@ final class ActivityViewerTasksMenuTests: XCTestCase { saveButton.click() workspaceSettingsWindow.buttons["Done"].click() - XCTAssertFalse(workspaceSettingsWindow.waitForNonExistence(timeout: 2.0), "Workspace Settings should have dismissed") + XCTAssertFalse( + workspaceSettingsWindow.waitForNonExistence(timeout: 2.0), + "Workspace Settings should have dismissed" + ) // Ensure the new task was added as an option XCTAssertEqual(taskDropdown.value as? String, "New Test Task") From f1fad8c6e7d4ad5c23562c0aaa3087fa6346825c Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:38:00 -0600 Subject: [PATCH 11/12] Add `waitForNonExistence` Backport --- CodeEdit.xcodeproj/project.pbxproj | 12 +++++++++ .../XCUITest+waitForNonExistence.swift | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 71324988bd..e8db9aabe7 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -403,6 +403,7 @@ 6C48D8F72972E5F300D6D205 /* WindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */; }; 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */; }; 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; }; + 6C510CB82D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C510CB72D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift */; }; 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; @@ -1095,6 +1096,7 @@ 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+Window.swift"; sourceTree = ""; }; 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalSidebar.swift; sourceTree = ""; }; + 6C510CB72D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUITest+waitForNonExistence.swift"; sourceTree = ""; }; 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ContentInsets.swift"; sourceTree = ""; }; 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionActivatorView.swift; sourceTree = ""; }; @@ -2966,6 +2968,14 @@ path = Environment; sourceTree = ""; }; + 6C510CB62D2E462D006EBE85 /* Extensions */ = { + isa = PBXGroup; + children = ( + 6C510CB72D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 6C6BD6ED29CD123000235D17 /* Extensions */ = { isa = PBXGroup; children = ( @@ -3031,6 +3041,7 @@ 6C96191F2C3F27E3009733CE /* CodeEditUITests */ = { isa = PBXGroup; children = ( + 6C510CB62D2E462D006EBE85 /* Extensions */, 6CFBA54A2C4E168A00E3A914 /* App.swift */, 6C9619232C3F2809009733CE /* ProjectPath.swift */, 6C9619212C3F27F1009733CE /* Query.swift */, @@ -4596,6 +4607,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6C510CB82D2E4639006EBE85 /* XCUITest+waitForNonExistence.swift in Sources */, 6C9619242C3F2809009733CE /* ProjectPath.swift in Sources */, 6CFBA54B2C4E168A00E3A914 /* App.swift in Sources */, 6CFBA54D2C4E16C900E3A914 /* WindowCloseCommandTests.swift in Sources */, diff --git a/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift b/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift new file mode 100644 index 0000000000..eb2d5cc48f --- /dev/null +++ b/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift @@ -0,0 +1,26 @@ +// +// XCUITest+waitForNonExistence.swift +// CodeEditUITests +// +// Created by Khan Winter on 1/7/25. +// + +import XCTest + +// Backport to Xcode 15, this exists in Xcode 16. + +extension XCUIElement { + /// Waits the specified amount of time for the element’s `exists` property to become `false`. + /// - Parameter timeout: The amount of time to wait. + /// - Returns: `false` if the timeout expires without the element coming out of existence. + /// + func waitForNonExistence(timeout: TimeInterval) -> Bool { + let timeStart = Date().timeIntervalSince1970 + + while Date().timeIntervalSince1970 <= (timeStart + timeout) { + if !exists { return true } + } + + return false + } +} From eabe10c71bd6a2333fcef47ff5eb10c21d3afffe Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:49:57 -0600 Subject: [PATCH 12/12] Better Waiting --- CodeEdit.xcodeproj/project.pbxproj | 6 +++--- .../Extensions/XCUITest+waitForNonExistence.swift | 13 ++++++------- .../ActivityViewer/Tasks/TasksMenuUITests.swift | 8 ++++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index e8db9aabe7..4fd9c12192 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3041,12 +3041,12 @@ 6C96191F2C3F27E3009733CE /* CodeEditUITests */ = { isa = PBXGroup; children = ( - 6C510CB62D2E462D006EBE85 /* Extensions */, 6CFBA54A2C4E168A00E3A914 /* App.swift */, - 6C9619232C3F2809009733CE /* ProjectPath.swift */, - 6C9619212C3F27F1009733CE /* Query.swift */, + 6C510CB62D2E462D006EBE85 /* Extensions */, 6C96191E2C3F27E3009733CE /* Features */, 6CFBA54E2C4E182100E3A914 /* Other Tests */, + 6C9619232C3F2809009733CE /* ProjectPath.swift */, + 6C9619212C3F27F1009733CE /* Query.swift */, ); path = CodeEditUITests; sourceTree = ""; diff --git a/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift b/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift index eb2d5cc48f..0533efeb52 100644 --- a/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift +++ b/CodeEditUITests/Extensions/XCUITest+waitForNonExistence.swift @@ -13,14 +13,13 @@ extension XCUIElement { /// Waits the specified amount of time for the element’s `exists` property to become `false`. /// - Parameter timeout: The amount of time to wait. /// - Returns: `false` if the timeout expires without the element coming out of existence. - /// func waitForNonExistence(timeout: TimeInterval) -> Bool { - let timeStart = Date().timeIntervalSince1970 - - while Date().timeIntervalSince1970 <= (timeStart + timeout) { - if !exists { return true } + let predicate = NSPredicate(format: "exists == false") + switch XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: predicate, object: self)], timeout: timeout) { + case .completed: + return true + default: + return false } - - return false } } diff --git a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift index c4ef81710d..a05e567dff 100644 --- a/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift +++ b/CodeEditUITests/Features/ActivityViewer/Tasks/TasksMenuUITests.swift @@ -45,14 +45,14 @@ final class ActivityViewerTasksMenuTests: XCTestCase { let taskDropdown = viewer.buttons["Active Task"] taskDropdown.click() let popover = app.popovers.firstMatch - XCTAssertTrue(popover.waitForExistence(timeout: 2.0), "Popover did not appear on click") + XCTAssertTrue(popover.exists, "Popover did not appear on click") let addTaskListOption = popover.buttons["Add Task..."] XCTAssertTrue(addTaskListOption.exists, "No add task option in dropdown") addTaskListOption.click() let workspaceSettingsWindow = window.sheets["Workspace Settings"] - XCTAssertTrue(workspaceSettingsWindow.waitForExistence(timeout: 2.0), "Workspace settings did not appear") + XCTAssertTrue(workspaceSettingsWindow.waitForExistence(timeout: 1.0), "Workspace settings did not appear") let addTaskButton = workspaceSettingsWindow.buttons["Add Task..."] XCTAssertTrue(addTaskButton.exists, "No add task button") @@ -60,7 +60,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { // Enter in task information let newSheet = workspaceSettingsWindow.sheets.firstMatch - XCTAssertTrue(newSheet.waitForExistence(timeout: 2.0), "New task sheet did not appear") + XCTAssertTrue(newSheet.waitForExistence(timeout: 1.0), "New task sheet did not appear") let taskName = newSheet.textFields["Task Name"] XCTAssertTrue(taskName.exists) taskName.click() @@ -79,7 +79,7 @@ final class ActivityViewerTasksMenuTests: XCTestCase { workspaceSettingsWindow.buttons["Done"].click() XCTAssertFalse( - workspaceSettingsWindow.waitForNonExistence(timeout: 2.0), + workspaceSettingsWindow.waitForNonExistence(timeout: 1.0), "Workspace Settings should have dismissed" )