初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
178
vendor/libghostty-spm/Example/GhosttyTerminalAppUITests/GhosttyTerminalAppUITests.swift
vendored
Normal file
178
vendor/libghostty-spm/Example/GhosttyTerminalAppUITests/GhosttyTerminalAppUITests.swift
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
import XCTest
|
||||
import AppKit
|
||||
|
||||
final class GhosttyTerminalAppUITests: XCTestCase {
|
||||
private var app: XCUIApplication!
|
||||
private var systemAlertMonitor: NSObjectProtocol?
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app = XCUIApplication()
|
||||
app.launchArguments = ["--ui-testing"]
|
||||
systemAlertMonitor = installSystemAlertHandler()
|
||||
app.launch()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
capture("final-state")
|
||||
app = nil
|
||||
}
|
||||
|
||||
func testTerminalUserOperations() throws {
|
||||
let terminal = try requireTerminalInteractionTarget()
|
||||
|
||||
capture("01-launch")
|
||||
clickTerminal(in: terminal)
|
||||
capture("02-focus")
|
||||
|
||||
typeTerminalText("echo mac-single\n", in: terminal)
|
||||
capture("03-single-line-input")
|
||||
|
||||
typeTerminalText("echo mac first line\n", in: terminal)
|
||||
typeTerminalText("echo mac second line\n", in: terminal)
|
||||
capture("04-multiple-lines")
|
||||
|
||||
typeTerminalText("中文键盘测试,标点和全角字符。\n", in: terminal)
|
||||
capture("05-chinese-input")
|
||||
|
||||
typeTerminalText("日本語キーボードテスト、かなと漢字。\n", in: terminal)
|
||||
capture("06-japanese-input")
|
||||
|
||||
typeTerminalText("Mixed input: English 中文 日本語 123\n", in: terminal)
|
||||
capture("07-multilingual-input")
|
||||
|
||||
terminal.swipeUp()
|
||||
capture("08-swipe-up")
|
||||
terminal.swipeDown()
|
||||
capture("09-swipe-down")
|
||||
|
||||
app.typeKey("=", modifierFlags: .command)
|
||||
capture("10-keyboard-zoom-in")
|
||||
app.typeKey("-", modifierFlags: .command)
|
||||
capture("11-keyboard-zoom-out")
|
||||
|
||||
typeTerminalText("clear\n", in: terminal)
|
||||
capture("12-clear-command")
|
||||
|
||||
assertNoCopyMenuWithoutSelection(in: terminal)
|
||||
typeTerminalText("echo selection anchor\n", in: terminal)
|
||||
dragPointerSelection(in: terminal)
|
||||
capture("13-pointer-selection")
|
||||
openCopyMenuAndCopySelection(in: terminal, screenshotName: "14-pointer-copy-menu")
|
||||
longPressTerminal(in: terminal)
|
||||
capture("15-long-press")
|
||||
}
|
||||
|
||||
private func requireTerminalInteractionTarget() throws -> XCUIElement {
|
||||
let terminal = app.descendants(matching: .any)["terminal.surface"].firstMatch
|
||||
if terminal.waitForExistence(timeout: 4), terminal.isHittable {
|
||||
return terminal
|
||||
}
|
||||
|
||||
let window = app.windows.firstMatch
|
||||
XCTAssertTrue(window.waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(window.isHittable)
|
||||
return window
|
||||
}
|
||||
|
||||
private func clickTerminal(in element: XCUIElement) {
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.55)).click()
|
||||
}
|
||||
|
||||
private func installSystemAlertHandler() -> NSObjectProtocol {
|
||||
addUIInterruptionMonitor(withDescription: "System alert") { alert in
|
||||
let preferredButtons = [
|
||||
"OK", "Ok", "好", "确定", "允许", "Allow", "继续", "Continue",
|
||||
"关闭", "Close", "Dismiss",
|
||||
]
|
||||
for title in preferredButtons {
|
||||
let button = alert.buttons[title].firstMatch
|
||||
if button.exists {
|
||||
button.click()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func typeTerminalText(_ text: String, in element: XCUIElement) {
|
||||
clickTerminal(in: element)
|
||||
element.typeText(text)
|
||||
}
|
||||
|
||||
private func longPressTerminal(in element: XCUIElement) {
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.55)).press(forDuration: 0.7)
|
||||
}
|
||||
|
||||
private func dragPointerSelection(in element: XCUIElement) {
|
||||
log("pointer-selection-coordinates", "start=(0.008, 0.10), end=(0.42, 0.10), rightClick=(0.20, 0.10)")
|
||||
let start = element.coordinate(withNormalizedOffset: CGVector(dx: 0.008, dy: 0.10))
|
||||
let end = element.coordinate(withNormalizedOffset: CGVector(dx: 0.42, dy: 0.10))
|
||||
start.press(forDuration: 0.1, thenDragTo: end)
|
||||
}
|
||||
|
||||
private func openCopyMenuAndCopySelection(in element: XCUIElement, screenshotName: String) {
|
||||
NSPasteboard.general.clearContents()
|
||||
disableSystemAlertMonitorBeforeContextMenu()
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.20, dy: 0.10)).rightClick()
|
||||
let copy = copyMenuItem()
|
||||
if !copy.waitForExistence(timeout: 3) {
|
||||
capture("\(screenshotName)-missing")
|
||||
XCTFail("Copy menu item not found after pointer selection right click. Hierarchy: \(app.debugDescription)")
|
||||
return
|
||||
}
|
||||
copy.click()
|
||||
capture(screenshotName)
|
||||
let actual = copiedPasteboardText(timeout: 2)
|
||||
log("pointer-selection-pasteboard", actual ?? "<nil>")
|
||||
XCTAssertEqual(actual, "selection anchor")
|
||||
}
|
||||
|
||||
private func assertNoCopyMenuWithoutSelection(in element: XCUIElement) {
|
||||
disableSystemAlertMonitorBeforeContextMenu()
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.20, dy: 0.10)).rightClick()
|
||||
XCTAssertFalse(
|
||||
copyMenuItem().waitForExistence(timeout: 0.5),
|
||||
"Copy menu item appeared without an active terminal selection."
|
||||
)
|
||||
}
|
||||
|
||||
private func disableSystemAlertMonitorBeforeContextMenu() {
|
||||
if let systemAlertMonitor {
|
||||
removeUIInterruptionMonitor(systemAlertMonitor)
|
||||
self.systemAlertMonitor = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func copyMenuItem() -> XCUIElement {
|
||||
app.menuItems["Copy"].firstMatch
|
||||
}
|
||||
|
||||
private func copiedPasteboardText(timeout: TimeInterval) -> String? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
repeat {
|
||||
if let string = NSPasteboard.general.string(forType: .string) {
|
||||
return string
|
||||
}
|
||||
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
|
||||
} while Date() < deadline
|
||||
return nil
|
||||
}
|
||||
|
||||
private func log(_ name: String, _ value: String) {
|
||||
let attachment = XCTAttachment(string: value)
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
|
||||
private func capture(_ name: String) {
|
||||
guard let app else { return }
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user