Files
Termind/TFITests/MobileUserInputSpeechLayoutTests.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

105 lines
5.0 KiB
Swift

import AgentKit
import SwiftUI
import TermindApplication
import TermindPresentation
import Testing
import UIKit
@testable import TermindIOS
@Suite(.serialized) @MainActor
struct MobileUserInputSpeechLayoutTests {
@Test(arguments: [false, true])
func recordingAndFinalizationKeepTwoTouchTargetsWithinACompactAnswer(recording: Bool) {
let controls = UserInputSpeechControls(
isActive: true, isRecording: recording, canStart: false, start: {}, cancel: {}, stop: {})
let host = UIHostingController(rootView: controls)
let size = host.sizeThatFits(in: CGSize(width: 320, height: 100))
#expect(size.width >= 88 && size.width <= 112)
#expect(size.height >= 44 && size.height <= 60)
}
@Test(
.enabled(if: ProcessInfo.processInfo.environment["TERMIND_ASSISTANT_SCREENSHOTS"] == "1"),
arguments: ["en", "zh-Hans", "zh-Hant"], [320.0, 768.0])
func captureAnswerFieldsAcrossSizesAndSpeechStates(language: String, width: Double) async throws {
let app = try MobileAppModel(inMemory: true)
let service = MobileQuestionSpeechService()
let speech = SpeechTranscriptionCoordinator(service: service)
for custom in [false, true] {
let conversation = ConversationModel()
conversation.present(
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.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 = MobileAssistantInteraction(
conversation: conversation, hosts: app.library, speech: speech,
interaction: UserInputInteractionModel(speech: session)
)
.environment(\.locale, Locale(identifier: language))
.environment(\.dynamicTypeSize, .large)
.environment(\.horizontalSizeClass, width < 400 ? .compact : .regular)
.padding(12)
.frame(width: width)
.background(.background)
let host = UIHostingController(rootView: content)
host.safeAreaRegions = []
let size = host.sizeThatFits(in: CGSize(width: width, height: 600))
#expect(abs(size.width - width) < 0.5)
#expect(size.height > 80 && size.height < 600)
// A non-key window lets UIKit-backed text fields render, without
// taking over the process-wide presentation/key-window state.
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
let window = UIWindow(windowScene: scene)
window.frame = CGRect(origin: .zero, size: size)
window.rootViewController = host
window.isHidden = false
defer {
window.isHidden = true
window.rootViewController = nil
}
window.layoutIfNeeded()
try await Task.sleep(for: .milliseconds(50))
let image = UIGraphicsImageRenderer(size: size).image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
let png = try #require(image.pngData())
Attachment.record(png, named: "question-ios-\(language)-\(Int(width))-\(custom)-\(phase).png")
session.cancel()
}
conversation.cancelUserInput()
}
}
}
@MainActor
private final class MobileQuestionSpeechService: SpeechTranscriptionService {
func requestAuthorization() async throws {}
func start(locale: Locale, eventHandler: @escaping @MainActor (SpeechTranscriptionEvent) -> Void) throws {}
func finish() {}
func cancel() {}
}