Skip to content

Commit c7a1928

Browse files
committed
✅ Initial UI tests
1 parent e043a6c commit c7a1928

File tree

8 files changed

+164
-119
lines changed

8 files changed

+164
-119
lines changed

DraftPatch/Components/ModelPickerPopoverView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ struct ModelPickerPopoverView: View {
210210
isPopoverPresented = false
211211

212212
Task {
213-
await viewModel.loadOllamaModels()
213+
await viewModel.loadLLMs()
214214
}
215215
}
216216
}

DraftPatch/DraftPatchApp.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,44 @@ struct DraftPatchApp: App {
2121
Settings.self,
2222
])
2323

24-
let configuration = ModelConfiguration(
25-
schema: schema,
26-
isStoredInMemoryOnly: false
27-
)
24+
if ProcessInfo.processInfo.arguments.contains("UI_TEST_MODE") {
25+
let testConfiguration = ModelConfiguration(
26+
schema: schema,
27+
isStoredInMemoryOnly: true
28+
)
2829

29-
self.modelContainer = try ModelContainer(
30-
for: ChatThread.self, ChatMessage.self, Settings.self,
31-
configurations: configuration
32-
)
30+
self.modelContainer = try ModelContainer(
31+
for: ChatThread.self, ChatMessage.self, Settings.self,
32+
configurations: testConfiguration
33+
)
34+
} else {
35+
let configuration = ModelConfiguration(
36+
schema: schema,
37+
isStoredInMemoryOnly: false
38+
)
39+
40+
self.modelContainer = try ModelContainer(
41+
for: ChatThread.self, ChatMessage.self, Settings.self,
42+
configurations: configuration
43+
)
44+
}
3345
} catch {
3446
fatalError("Error creating ModelContainer: \(error)")
3547
}
3648

3749
let ctx = ModelContext(self.modelContainer)
3850
let repository = SwiftDataChatThreadRepository(context: ctx)
39-
_viewModel = StateObject(wrappedValue: DraftPatchViewModel(repository: repository))
51+
52+
if ProcessInfo.processInfo.arguments.contains("UI_TEST_MODE") {
53+
_viewModel = StateObject(
54+
wrappedValue: DraftPatchViewModel(
55+
repository: repository,
56+
llmManager: MockLLMManager()
57+
)
58+
)
59+
} else {
60+
_viewModel = StateObject(wrappedValue: DraftPatchViewModel(repository: repository))
61+
}
4062

4163
// Request accessibility permissions for drafting
4264
DraftingService.shared.checkAccessibilityPermission()

DraftPatch/DraftPatchViewModel.swift

Lines changed: 10 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class DraftPatchViewModel: ObservableObject {
3939
@Published var settings: Settings? = nil
4040
@Published var errorMessage: String? = nil
4141

42-
init(repository: ChatThreadRepository, llmManager: LLMManager = .shared) {
42+
init(repository: ChatThreadRepository, llmManager: LLMManager = LLMManager.shared) {
4343
self.repository = repository
4444
self.llmManager = llmManager
4545

@@ -100,78 +100,19 @@ class DraftPatchViewModel: ObservableObject {
100100
}
101101

102102
func loadLLMs() async {
103-
self.availableModels = []
104-
105-
await loadOllamaModels()
106-
await loadOpenAIModels()
107-
await loadGeminiModels()
108-
await loadAnthropicModels()
109-
}
110-
111-
func loadOllamaModels() async {
112-
guard settings?.ollamaConfig?.enabled ?? false else { return }
113-
114-
do {
115-
let models = try await OllamaService.shared.fetchAvailableModels()
116-
let ollamaModels = models.map { ChatModel(name: $0, provider: .ollama) }
117-
addModels(ollamaModels)
118-
} catch {
119-
print("Error loading Ollama models: \(error)")
120-
}
121-
}
122-
123-
func loadOpenAIModels() async {
124-
guard settings?.openAIConfig?.enabled ?? false else { return }
125-
126-
do {
127-
let models = try await OpenAIService.shared.fetchAvailableModels()
128-
let openAIModels = models.map { ChatModel(name: $0, provider: .openai) }
129-
addModels(openAIModels)
130-
} catch {
131-
print("Error loading OpenAI models: \(error)")
132-
}
133-
}
134-
135-
func loadGeminiModels() async {
136-
guard settings?.geminiConfig?.enabled ?? false else { return }
137-
138-
do {
139-
let models = try await GeminiService.shared.fetchAvailableModels()
140-
let geminiModels = models.map { ChatModel(name: $0, provider: .gemini) }
141-
addModels(geminiModels)
142-
} catch {
143-
print("Error loading Gemini models: \(error)")
144-
}
145-
}
146-
147-
func loadAnthropicModels() async {
148-
guard settings?.anthropicConfig?.enabled ?? false else { return }
149-
150-
do {
151-
let models = try await ClaudeService.shared.fetchAvailableModels()
152-
let anthropicModels = models.map { ChatModel(name: $0, provider: .anthropic) }
153-
addModels(anthropicModels)
154-
} catch {
155-
print("Error loading Anthropic models: \(error)")
156-
}
157-
}
158-
159-
private func addModels(_ newModels: [ChatModel]) {
160-
for model in newModels {
161-
if !self.availableModels.contains(model) {
162-
self.availableModels.append(model)
163-
}
164-
}
103+
self.availableModels = await llmManager.loadLLMs(settings)
165104
}
166105

167106
func toggleDraftWithLastApp() {
168-
if let lastAppDraftedWith = settings?.lastAppDraftedWith {
169-
if isDraftingEnabled {
170-
isDraftingEnabled = false
171-
selectedDraftApp = nil
172-
} else {
173-
isDraftingEnabled = true
107+
if isDraftingEnabled {
108+
isDraftingEnabled = false
109+
selectedDraftApp = nil
110+
} else {
111+
isDraftingEnabled = true
112+
if let lastAppDraftedWith = settings?.lastAppDraftedWith {
174113
selectedDraftApp = lastAppDraftedWith
114+
} else {
115+
selectedDraftApp = DraftApp(rawValue: "Xcode")
175116
}
176117
}
177118
}

DraftPatch/Services/LLMManager.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,35 @@ class LLMManager {
2020
return ClaudeService.shared
2121
}
2222
}
23+
24+
func loadLLMs(_ settings: Settings?) async -> [ChatModel] {
25+
var availableModels: [ChatModel] = []
26+
27+
let providers: [(enabled: Bool, service: LLMService, provider: ChatModel.LLMProvider)] = [
28+
(settings?.ollamaConfig?.enabled ?? false, OllamaService.shared, .ollama),
29+
(settings?.openAIConfig?.enabled ?? false, OpenAIService.shared, .openai),
30+
(settings?.geminiConfig?.enabled ?? false, GeminiService.shared, .gemini),
31+
(settings?.anthropicConfig?.enabled ?? false, ClaudeService.shared, .anthropic),
32+
]
33+
34+
await withTaskGroup(of: [ChatModel].self) { group in
35+
for provider in providers where provider.enabled {
36+
group.addTask {
37+
do {
38+
let models = try await provider.service.fetchAvailableModels()
39+
return models.map { ChatModel(name: $0, provider: provider.provider) }
40+
} catch {
41+
print("Error loading \(provider.provider) models: \(error)")
42+
return []
43+
}
44+
}
45+
}
46+
47+
for await models in group {
48+
availableModels.append(contentsOf: models)
49+
}
50+
}
51+
52+
return Array(Set(availableModels)) // Remove duplicates if any
53+
}
2354
}

DraftPatch/Services/MockLLMService.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77

88
import Foundation
99

10+
class MockLLMManager: LLMManager {
11+
override func getService(for provider: ChatModel.LLMProvider) -> LLMService {
12+
return MockLLMService.shared
13+
}
14+
15+
override func loadLLMs(_ settings: Settings?) async -> [ChatModel] {
16+
return try! await MockLLMService.shared.fetchAvailableModels().map {
17+
ChatModel(name: $0, provider: .ollama)
18+
}
19+
}
20+
}
21+
1022
class MockLLMService: LLMService {
1123
static let shared = MockLLMService()
1224

DraftPatch/Views/ChatView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ struct ChatView: View {
1616
if let thread = viewModel.selectedThread {
1717
VStack {
1818
VStack(spacing: 0) {
19+
if thread.messages.isEmpty {
20+
Text("No messages yet")
21+
.foregroundColor(.gray).opacity(0.01)
22+
.padding()
23+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
24+
}
25+
1926
ScrollView {
2027
VStack(spacing: 0) {
2128
VStack(spacing: 8) {

DraftPatchTests/DraftPatchTests.swift

Lines changed: 0 additions & 18 deletions
This file was deleted.

DraftPatchUITests/DraftPatchUITests.swift

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,10 @@
88
import XCTest
99

1010
final class DraftPatchUITests: XCTestCase {
11-
// override func setUpWithError() throws {
12-
// // Put setup code here. This method is called before the invocation of each test method in the class.
13-
//
14-
// // In UI tests it is usually best to stop immediately when a failure occurs.
15-
// continueAfterFailure = false
16-
//
17-
// // 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.
18-
// }
19-
//
20-
// override func tearDownWithError() throws {
21-
// // Put teardown code here. This method is called after the invocation of each test method in the class.
22-
// }
23-
//
24-
// @MainActor
25-
// func testExample() throws {
26-
// // UI tests must launch the application that they test.
27-
// let app = XCUIApplication()
28-
// app.launch()
29-
//
30-
// // Use XCTAssert and related functions to verify your tests produce the correct results.
31-
// }
32-
//
11+
override func setUpWithError() throws {
12+
continueAfterFailure = false
13+
}
14+
3315
// @MainActor
3416
// func testLaunchPerformance() throws {
3517
// if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
@@ -39,4 +21,72 @@ final class DraftPatchUITests: XCTestCase {
3921
// }
4022
// }
4123
// }
24+
25+
@MainActor
26+
func testCommandNCreatesNewChatThread() throws {
27+
let app = XCUIApplication()
28+
app.launchArguments.append("UI_TEST_MODE")
29+
app.launch()
30+
31+
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5))
32+
33+
app.typeKey("n", modifierFlags: .command)
34+
35+
// Optionally, ensure new thread is selected
36+
app.typeKey("d", modifierFlags: .command)
37+
38+
let newConversationTitle = app.staticTexts["New Conversation"]
39+
XCTAssertTrue(
40+
newConversationTitle.waitForExistence(timeout: 2), "The chat title is not 'New Conversation'")
41+
42+
let noMessagesPlaceholder = app.staticTexts["No messages yet"]
43+
XCTAssertTrue(
44+
noMessagesPlaceholder.waitForExistence(timeout: 2),
45+
"Placeholder for no messages should exist in new conversation")
46+
}
47+
48+
@MainActor
49+
func testAppLaunchShowsEmptyState() throws {
50+
let app = XCUIApplication()
51+
app.launchArguments.append("UI_TEST_MODE")
52+
app.launch()
53+
54+
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5))
55+
56+
let noChatSelectedText = app.staticTexts["No chat selected"]
57+
XCTAssertTrue(
58+
noChatSelectedText.waitForExistence(timeout: 2), "The 'No chat selected' text is not visible")
59+
60+
let startDraftingText = app.staticTexts["Select a chat and start drafting!"]
61+
XCTAssertTrue(
62+
startDraftingText.waitForExistence(timeout: 2),
63+
"The 'Select a chat and start drafting!' text is not visible")
64+
65+
let checkeredFlagImage = app.images["flag.checkered"]
66+
XCTAssertTrue(checkeredFlagImage.waitForExistence(timeout: 2), "The checkered flag image is not visible")
67+
}
68+
69+
@MainActor
70+
func testCommandDShowsDraftingText() throws {
71+
let app = XCUIApplication()
72+
app.launchArguments.append("UI_TEST_MODE")
73+
app.launch()
74+
75+
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5))
76+
77+
// Create a new chat using Command + N
78+
app.typeKey("n", modifierFlags: .command)
79+
80+
let title = app.staticTexts["New Conversation"]
81+
XCTAssertTrue(title.waitForExistence(timeout: 2), "The chat title is not 'New Conversation'")
82+
83+
// Press Command + D
84+
app.typeKey("d", modifierFlags: .command)
85+
86+
// Verify "Drafting with" text appears
87+
let draftingTextElement = app.staticTexts["Drafting with Xcode • Unknown"]
88+
XCTAssertTrue(draftingTextElement.waitForExistence(timeout: 2), "The 'Drafting with' text did not appear")
89+
90+
app.typeKey("d", modifierFlags: .command)
91+
}
4292
}

0 commit comments

Comments
 (0)