Files
Termind/TFMTests/UserInputSpeechLayoutTests.swift
T
Kaede Akino 82d871c4fe refactor: separate feature policies and UI workflows
Move connection policy out of ProAccess, keep terminal uploads with their
session, and share question interaction and highlight editing operations.
Consolidate CPU usage and settings snapshot handling, and remove obsolete
terminal selection, theme helpers, and hard-coded latest-schema assertions.

Fix draining connection-pool recovery and stale pane upload bindings. Keep
screenshot matrices opt-in and retain behavioral migration coverage.

Validation: 2468 macOS tests passed serially, 31 iOS tests passed, and both
SSH forwarding regressions passed against the local OpenSSH fixture.
Both app builds, Swift formatting, AppStrings, and Chinese localization
checks passed. Parallel test runs still exhibit intermittent timeouts.
2026-10-02 16:40:27 +08:00

123 lines
5.8 KiB
Swift

import AgentKit
import AppKit
import SwiftUI
import TermindApplication
import TermindPresentation
import Testing
@testable import Termind
@Suite @MainActor
struct UserInputSpeechLayoutTests {
@Test(arguments: [false, true])
func recordingAndFinalizationReserveSpaceForBothControls(recording: Bool) {
let controls = UserInputSpeechControls(
isActive: true, isRecording: recording, canStart: false, start: {}, cancel: {}, stop: {})
let host = NSHostingController(rootView: controls)
let size = host.view.fittingSize
// Two independent actions must keep their hit areas while leaving room for the answer.
#expect(size.width >= 64 && size.width <= 96)
#expect(size.height >= 32 && size.height <= 44)
}
@Test(
.enabled(if: ProcessInfo.processInfo.environment["TERMIND_ASSISTANT_SCREENSHOTS"] == "1"),
arguments: [false, true])
func captureSubmitAndContinueButtonStates(isDark: Bool) throws {
let content = VStack(spacing: 16) {
ForEach([true, false], id: \.self) { enabled in
HStack(spacing: 12) {
Button(AppStrings.Common.submit.swiftUIKey) {}
.transcriptProminentButtonStyle()
.disabled(!enabled)
Button(AppStrings.Common.next.swiftUIKey) {}
.transcriptProminentButtonStyle()
.disabled(!enabled)
Button(AppStrings.Common.continue.swiftUIKey) {}
.transcriptProminentButtonStyle()
.disabled(!enabled)
}
}
}
.padding(20)
.background(.background)
.environment(\.colorScheme, isDark ? .dark : .light)
let host = NSHostingController(rootView: content)
host.view.appearance = NSAppearance(named: isDark ? .darkAqua : .aqua)
host.view.frame = NSRect(origin: .zero, size: host.view.fittingSize)
host.view.layoutSubtreeIfNeeded()
let bitmap = try #require(host.view.bitmapImageRepForCachingDisplay(in: host.view.bounds))
host.view.cacheDisplay(in: host.view.bounds, to: bitmap)
let png = try #require(bitmap.representation(using: .png, properties: [:]))
Attachment.record(png, named: "question-button-states-\(isDark).png")
}
@Test(
.enabled(if: ProcessInfo.processInfo.environment["TERMIND_ASSISTANT_SCREENSHOTS"] == "1"),
arguments: ["en", "zh-Hans", "zh-Hant"], [360.0, 680.0])
func captureAnswerFieldsAcrossSizesAndSpeechStates(language: String, width: Double) async throws {
let service = DesktopQuestionSpeechService()
let speech = SpeechTranscriptionCoordinator(service: service)
let app = AppModel(hosts: HostLibraryModel(), speechTranscription: speech)
for custom in [false, true] {
let conversation = ConversationModel()
let request = AgentUserInputRequest(
questions: [
AgentUserQuestion(
id: "answer", prompt: "Where should this run?",
kind: custom
? .choice(AgentUserInputChoice(options: [AgentUserInputOption(id: "a", label: "Staging")]))
: .text)
]
+ (custom
? [
AgentUserQuestion(id: "second", prompt: "Second question", kind: .text),
AgentUserQuestion(id: "third", prompt: "Third question", kind: .text),
] : []))
conversation.present(request)
conversation.setUserInputCustomAnswer("Existing answer", for: "answer")
for phase in ["idle", "recording", "transcribing"] {
let session = UserInputSpeechSession()
if phase != "idle" {
session.start(coordinator: speech, conversation: conversation) {}
for _ in 0..<100 {
if session.isRecording { break }
try await Task.sleep(for: .milliseconds(5))
}
try #require(session.isRecording)
}
if phase == "transcribing" { session.stop() }
let content = AgentUserInputCard(
request: request, conversation: conversation, isCompact: width < 400,
interaction: UserInputInteractionModel(speech: session)
)
.environment(app)
.environment(\.locale, Locale(identifier: language))
.padding(16)
.frame(width: width)
.background(.background)
let host = NSHostingController(rootView: content)
let size = host.view.fittingSize
#expect(abs(size.width - width) < 0.5)
#expect(size.height > 80 && size.height < 360)
host.view.frame = NSRect(origin: .zero, size: size)
host.view.layoutSubtreeIfNeeded()
let bitmap = try #require(host.view.bitmapImageRepForCachingDisplay(in: host.view.bounds))
host.view.cacheDisplay(in: host.view.bounds, to: bitmap)
let png = try #require(bitmap.representation(using: .png, properties: [:]))
Attachment.record(png, named: "question-mac-\(language)-\(Int(width))-\(custom)-\(phase).png")
session.cancel()
}
conversation.cancelUserInput()
}
}
}
@MainActor
private final class DesktopQuestionSpeechService: SpeechTranscriptionService {
func requestAuthorization() async throws {}
func start(locale: Locale, eventHandler: @escaping @MainActor (SpeechTranscriptionEvent) -> Void) throws {}
func finish() {}
func cancel() {}
}