fix: preserve uncommitted translation input

This commit is contained in:
2026-10-25 15:44:44 +01:00
parent 47912d237c
commit ef979d2838
5 changed files with 96 additions and 5 deletions
@@ -1,4 +1,5 @@
import AppKit
import ConjureKit
import Foundation
import Testing
@testable import Conjure
@@ -146,11 +147,14 @@ func primaryActionCopiesCompletedTranslationAndResetsAfterEditing() async throws
#expect(!dismissed)
requests.pending[0].resume(returning: "こんにちは")
try await waitForTranslation { button.isEnabled }
#expect(tool.draft?.input == "Hello")
tool.performPrimaryAction()
#expect(pasteboard.string(forType: .string) == "こんにちは")
#expect(dismissed)
#expect(tool.draft == nil)
dismissed = false
tool.inputChanged()
#expect(tool.draft?.input == "Hello")
button.performClick(nil)
try await waitForTranslation { requests.pending.count == 2 }
#expect(!dismissed)
@@ -435,4 +439,73 @@ func blankContextTranslatesWithoutSwappingAndProviderChangesRestoreOriginalText(
requests.pending[2].resume(returning: "やあ")
try await waitForTranslation { output.string == "やあ" }
}
@Test(arguments: ["escape", "window-close", "deactivate", "switch"])
@MainActor func uncommittedInputSurvivesLeavingTranslate(_ exit: String) throws {
_ = NSApplication.shared
let suite = "TranslateDraft.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.set("openai", forKey: "translationProvider")
defer { defaults.removePersistentDomain(forName: suite) }
let launcher = LauncherWindowController(
history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }),
defaults: defaults)
let window = try #require(launcher.window)
defer { window.orderOut(nil) }
let content = try #require(window.contentView)
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first)
func tool() throws -> TranslationController {
try #require(descendants(content).compactMap { $0 as? NSButton }
.compactMap { $0.target as? TranslationController }.first)
}
func enter(_ text: String) {
search.stringValue = text
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
}
func leave() {
switch exit {
case "escape":
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
case "window-close":
window.performClose(nil)
case "deactivate":
launcher.windowDidResignKey(Notification(name: NSWindow.didResignKeyNotification, object: window))
default:
launcher.present(command: .emoji)
}
}
launcher.present(command: .translate)
enter(" An unfinished sentence 🌏\n")
leave()
launcher.present(command: .translate)
#expect(search.stringValue == " An unfinished sentence 🌏\n")
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
try tool().performPrimaryAction() // Advance to context without sending a request.
leave()
launcher.present(command: .translate)
#expect(search.stringValue.isEmpty)
#expect(search.accessibilityLabel() == "Context or situation for the translation (optional)")
enter("Said by a friend")
leave()
launcher.present(command: .translate)
#expect(search.stringValue == "Said by a friend")
#expect(search.accessibilityLabel() == "Context or situation for the translation (optional)")
let preview = try #require(descendants(content).compactMap { $0 as? NSTextField }
.first { $0.accessibilityLabel() == "Original text to translate" })
#expect(preview.stringValue == "Text to translate: An unfinished sentence 🌏\n")
try tool().goBack()
#expect(search.stringValue == " An unfinished sentence 🌏\n")
try tool().goBack() // Returning to Applications must also retain the draft.
search.stringValue = "translate"
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
#expect(search.stringValue == " An unfinished sentence 🌏\n")
enter("")
leave()
launcher.present(command: .translate)
#expect(search.stringValue.isEmpty) // Clearing a draft must not resurrect older text.
launcher.dismiss()
}
}