feat: complete translation language controls and styling
This commit is contained in:
@@ -38,6 +38,19 @@ import Testing
|
||||
#expect(translation == "こんにちは")
|
||||
}
|
||||
|
||||
@Test func myMemoryRequestsReversePairAndOnlyUsesMatchingFallback() async throws {
|
||||
let body = #"{"responseStatus":200,"responseData":{"translatedText":""},"matches":[{"segment":"おはよう","translation":"間違い","source":"en","target":"ja"},{"segment":"おはよう","translation":"bonjour","source":"ja","target":"fr"},{"segment":"おはようございます","translation":"Good morning!","source":"ja","target":"en"},{"segment":"おはよう","translation":"Good morning","source":"ja-JP","target":"en-GB"}]}"#
|
||||
let translation = try await MyMemoryAPI.translate(
|
||||
"おはよう", sourceLanguage: "ja", targetLanguage: "en"
|
||||
) { request in
|
||||
let items = try #require(URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?.queryItems)
|
||||
#expect(items.contains(URLQueryItem(name: "q", value: "おはよう")))
|
||||
#expect(items.contains(URLQueryItem(name: "langpair", value: "ja|en")))
|
||||
return (Data(body.utf8), HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
|
||||
}
|
||||
#expect(translation == "Good morning")
|
||||
}
|
||||
|
||||
@Test func myMemoryNeverUsesMatchesToBypassFailureOrReplacePrimaryTranslation() async throws {
|
||||
let matches = #""matches":[{"segment":"hello","translation":"こんにちは","source":"en","target":"ja"}]"#
|
||||
for metadata in [#""responseStatus":403"#, #""responseStatus":200,"quotaFinished":true"#] {
|
||||
|
||||
@@ -5,13 +5,13 @@ import Testing
|
||||
|
||||
@MainActor
|
||||
private final class TranslationRequests {
|
||||
var inputs: [(text: String, key: String?)] = []
|
||||
var inputs: [(text: String, source: String, target: String, key: String?)] = []
|
||||
var pending: [CheckedContinuation<String, any Error>] = []
|
||||
var returned = 0
|
||||
|
||||
// Deliberately ignore cancellation: the controller must reject late completions itself.
|
||||
func translate(_ text: String, key: String? = nil) async throws -> String {
|
||||
inputs.append((text, key))
|
||||
func translate(_ text: String, source: String, target: String, key: String? = nil) async throws -> String {
|
||||
inputs.append((text, source, target, key))
|
||||
defer { returned += 1 }
|
||||
return try await withCheckedThrowingContinuation { pending.append($0) }
|
||||
}
|
||||
@@ -58,6 +58,50 @@ private func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.fl
|
||||
|
||||
@Suite(.serialized)
|
||||
struct TranslationExecutionTests {
|
||||
@Test @MainActor
|
||||
func languageActionsInvalidateOldResultsAndApplyTheSelectedPair() async throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "TranslateLanguages.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let requests = TranslationRequests()
|
||||
let tool = TranslationController(defaults: defaults,
|
||||
myMemory: { try await requests.translate($0, source: $1, target: $2) })
|
||||
tool.input = { "こんにちは" }
|
||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 474),
|
||||
styleMask: .borderless, backing: .buffered, defer: false)
|
||||
window.isReleasedWhenClosed = false
|
||||
window.contentView = tool.view
|
||||
defer { tool.close(); window.orderOut(nil) }
|
||||
let output = try #require(descendants(tool.view).compactMap { $0 as? NSTextView }.first)
|
||||
tool.performPrimaryAction()
|
||||
try await waitForTranslation { requests.pending.count == 1 }
|
||||
let languages = try #require(descendants(tool.view).compactMap { $0 as? NSButton }
|
||||
.first { $0.accessibilityLabel() == "Translation languages" })
|
||||
languages.performClick(nil)
|
||||
let host = try #require(window.contentView)
|
||||
let search = try #require(descendants(host).compactMap { $0 as? NSSearchField }.first)
|
||||
search.stringValue = "swap"
|
||||
search.delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||
#expect(tool.handle(#selector(NSResponder.insertNewline(_:))))
|
||||
#expect(tool.sourceLanguage == "ja")
|
||||
#expect(tool.targetLanguage == "en")
|
||||
requests.pending[0].resume(returning: "obsolete")
|
||||
try await waitForTranslation { requests.returned == 1 }
|
||||
#expect(output.string.isEmpty)
|
||||
tool.performPrimaryAction()
|
||||
try await waitForTranslation { requests.pending.count == 2 }
|
||||
#expect(requests.inputs[1].source == "ja")
|
||||
#expect(requests.inputs[1].target == "en")
|
||||
requests.pending[1].resume(returning: "Hello")
|
||||
try await waitForTranslation { output.string == "Hello" }
|
||||
tool.setLanguages(source: "de", target: "fr")
|
||||
#expect(output.string.isEmpty)
|
||||
let reopened = TranslationController(defaults: defaults)
|
||||
#expect(reopened.sourceLanguage == "de")
|
||||
#expect(reopened.targetLanguage == "fr")
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func primaryActionCopiesCompletedTranslationAndResetsAfterEditing() async throws {
|
||||
_ = NSApplication.shared
|
||||
@@ -70,7 +114,7 @@ func primaryActionCopiesCompletedTranslationAndResetsAfterEditing() async throws
|
||||
}
|
||||
let requests = TranslationRequests()
|
||||
let tool = TranslationController(defaults: defaults,
|
||||
myMemory: { try await requests.translate($0) }, pasteboard: pasteboard)
|
||||
myMemory: { try await requests.translate($0, source: $1, target: $2) }, pasteboard: pasteboard)
|
||||
tool.input = { "Hello" }
|
||||
var dismissed = false
|
||||
tool.dismiss = { dismissed = true }
|
||||
@@ -107,8 +151,8 @@ func translationExecutionIsExplicitAndMyMemoryNeverTouchesCredentials() async th
|
||||
keys.value = "private-key"
|
||||
let requests = TranslationRequests()
|
||||
let tool = TranslationController(defaults: defaults, credentials: keys.store,
|
||||
myMemory: { try await requests.translate($0) },
|
||||
openAI: { try await requests.translate($0, key: $1) })
|
||||
myMemory: { try await requests.translate($0, source: $1, target: $2) },
|
||||
openAI: { try await requests.translate($0, source: $1, target: $2, key: $3) })
|
||||
tool.input = { "Hello" }
|
||||
let views = descendants(tool.view)
|
||||
let output = try #require(views.compactMap { $0 as? NSTextView }.first)
|
||||
@@ -135,8 +179,8 @@ func translationExecutionRejectsOldCompletionsWithoutClearingNewRequests() async
|
||||
keys.value = "saved-key"
|
||||
let requests = TranslationRequests()
|
||||
let tool = TranslationController(defaults: defaults, credentials: keys.store,
|
||||
myMemory: { try await requests.translate($0) },
|
||||
openAI: { try await requests.translate($0, key: $1) })
|
||||
myMemory: { try await requests.translate($0, source: $1, target: $2) },
|
||||
openAI: { try await requests.translate($0, source: $1, target: $2, key: $3) })
|
||||
var input = "first"
|
||||
tool.input = { input }
|
||||
let views = descendants(tool.view)
|
||||
@@ -190,8 +234,8 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
|
||||
let keys = TranslationKeys()
|
||||
let requests = TranslationRequests()
|
||||
let tool = TranslationController(defaults: defaults, credentials: keys.store,
|
||||
myMemory: { try await requests.translate($0) },
|
||||
openAI: { try await requests.translate($0, key: $1) })
|
||||
myMemory: { try await requests.translate($0, source: $1, target: $2) },
|
||||
openAI: { try await requests.translate($0, source: $1, target: $2, key: $3) })
|
||||
tool.input = { "Hello" }
|
||||
let views = descendants(tool.view)
|
||||
let settings = TranslationSettingsController(credentials: keys.store)
|
||||
|
||||
@@ -12,6 +12,8 @@ import Testing
|
||||
#expect(body["model"] as? String == "gpt-6-luna")
|
||||
#expect(body["input"] as? String == "Good morning")
|
||||
#expect(body["store"] as? Bool == false)
|
||||
let instructions = try #require(body["instructions"] as? String)
|
||||
#expect(instructions.contains("from en to ja"))
|
||||
return (Data(#"{"status":"completed","output":[{"type":"reasoning"},{"content":[{"type":"output_text","text":"おはようございます"}]}]}"#.utf8),
|
||||
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
|
||||
}
|
||||
@@ -26,6 +28,20 @@ import Testing
|
||||
}
|
||||
}
|
||||
|
||||
@Test func translationSendsReverseLanguagePairToOpenAI() async throws {
|
||||
let text = try await TranslationAPI.translate(
|
||||
"おはようございます", apiKey: "test-key", sourceLanguage: "ja", targetLanguage: "en"
|
||||
) { request in
|
||||
let body = try #require(JSONSerialization.jsonObject(with: request.httpBody!) as? [String: Any])
|
||||
let instructions = try #require(body["instructions"] as? String)
|
||||
#expect(instructions.contains("from ja to en"))
|
||||
#expect(body["input"] as? String == "おはようございます")
|
||||
return (Data(#"{"status":"completed","output":[{"content":[{"type":"output_text","text":"Good morning"}]}]}"#.utf8),
|
||||
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
|
||||
}
|
||||
#expect(text == "Good morning")
|
||||
}
|
||||
|
||||
@Test @MainActor func translationCommandBackNavigationRestoresSearch() throws {
|
||||
_ = NSApplication.shared
|
||||
let launcher = LauncherWindowController()
|
||||
|
||||
Reference in New Issue
Block a user