Files
Termind/TFMTests/SingleLineFieldEditingTests.swift

132 lines
5.6 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AppKit
import SwiftUI
import TermindDomain
import TermindPresentation
import Testing
@testable import Termind
@Suite("Native single-line field editing", .serialized)
@MainActor
struct SingleLineFieldEditingTests {
@Test func nativeEditorKeepsTheCaretAndDefersMarkedTextNormalization() throws {
let field = NSTextField()
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 100), styleMask: .borderless,
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.contentView = field
defer { window.close() }
#expect(window.makeFirstResponder(field))
let editor = try #require(field.currentEditor() as? NSTextView)
field.stringValue = "中文\r\n\nName"
editor.string = field.stringValue
editor.setSelectedRange(NSRange(location: 5, length: 0))
#expect(SingleLineFieldEditing.normalizedValue(in: field) == "中文 Name")
#expect(editor.string == "中文 Name")
#expect(editor.selectedRange() == NSRange(location: 3, length: 0))
#expect(window.firstResponder === editor)
editor.setMarkedText(
"拼音", selectedRange: NSRange(location: 2, length: 0), replacementRange: editor.selectedRange())
#expect(editor.hasMarkedText())
let composing = editor.string
#expect(SingleLineFieldEditing.normalizedValue(in: field) == nil)
#expect(editor.string == composing)
editor.unmarkText()
#expect(SingleLineFieldEditing.normalizedValue(in: field) == editor.string)
}
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func hostFormKeepsPastedAndTypedTextSingleLine(language: String) async throws {
let draft = SingleLineHostTestDraft()
let hosting = NSHostingView(
rootView: SingleLineHostTestForm(draft: draft).environment(\.locale, Locale(identifier: language)))
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 400), styleMask: .borderless,
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.contentView = hosting
defer { window.close() }
hosting.layoutSubtreeIfNeeded()
try await Task.sleep(for: .milliseconds(100))
let field = try #require(findEditableField(in: hosting, value: "fixture.invalid"))
#expect(window.makeFirstResponder(field))
let editor = try #require(field.currentEditor() as? NSTextView)
editor.selectAll(nil)
SingleLineFieldEditing.insert("first.example\r\n\nsecond.example", into: editor)
try await Task.sleep(for: .milliseconds(100))
#expect(draft.host.host == "first.example second.example")
#expect(!field.stringValue.contains(where: { $0.isNewline }))
#expect(window.firstResponder === editor)
editor.selectAll(nil)
editor.insertText("2001:db8::1", replacementRange: editor.selectedRange())
try await Task.sleep(for: .milliseconds(100))
#expect(draft.host.host == "2001:db8::1")
editor.doCommand(by: #selector(NSResponder.insertNewlineIgnoringFieldEditor(_:)))
try await Task.sleep(for: .milliseconds(100))
#expect(!draft.host.host.contains(where: { $0.isNewline }))
#expect(!field.stringValue.contains(where: { $0.isNewline }))
let originalHeight = field.frame.height
let longAddress = String(repeating: "long-host-", count: 80) + ".invalid"
editor.selectAll(nil)
SingleLineFieldEditing.insert(longAddress, into: editor)
try await Task.sleep(for: .milliseconds(100))
#expect(draft.host.host == longAddress)
#expect(field.frame.height == originalHeight)
}
private func findEditableField(in view: NSView, value: String) -> NSTextField? {
if let field = view as? NSTextField, field.isEditable, field.stringValue == value { return field }
return view.subviews.lazy.compactMap { findEditableField(in: $0, value: value) }.first
}
@Test func normalizesTheNativeFieldWithoutAnActiveEditor() {
let field = NSTextField()
field.stringValue = "中文\r\n\n名称"
#expect(SingleLineFieldEditing.normalizedValue(in: field) == "中文 名称")
#expect(field.stringValue == "中文 名称")
}
@Test func renameCoordinatorWritesTheSameNormalizedValueAsTheField() {
var text = "Original"
var submitted = false
var cancelled = false
let coordinator = SidebarInlineRenameField.Coordinator(
text: Binding(get: { text }, set: { text = $0 }),
onSubmit: { submitted = true }, onCancel: { cancelled = true }
)
let field = NSTextField()
field.stringValue = "New\u{2028}Name"
coordinator.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: field))
#expect(text == "New Name")
#expect(field.stringValue == text)
#expect(!submitted)
#expect(!cancelled)
}
}
@Observable
@MainActor
private final class SingleLineHostTestDraft {
var host = TermindDomain.Host(name: "Fixture", host: "fixture.invalid")
}
private struct SingleLineHostTestForm: View {
@Bindable var draft: SingleLineHostTestDraft
var body: some View {
Form {
HostOrganizationSection(host: $draft.host, detectedDistribution: draft.host.detectedDistribution) {
EmptyView()
} tagsField: {
EmptyView()
}
HostConnectionFields(host: $draft.host)
}
.formStyle(.grouped)
}
}