221 lines
9.9 KiB
Swift
221 lines
9.9 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindPresentation
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite(.requiresVisibleWindow, .serialized) @MainActor
|
|
struct WelcomeQuickConnectRenderingTests {
|
|
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
|
|
func welcomeGlassRowsRenderRecentsHostsAndOneOffConnections(_ language: String) async throws {
|
|
let localDeviceID = UUID()
|
|
let ubuntuDistribution = try #require(DetectedDistribution(id: "ubuntu"))
|
|
let unknownDistribution = try #require(DetectedDistribution(id: "termind-test-os"))
|
|
let ubuntu = Host(
|
|
name: "Ubuntu Server", host: "ubuntu.example", colorTag: .orange,
|
|
detectedDistribution: ubuntuDistribution
|
|
)
|
|
let unknown = Host(
|
|
name: "Unknown Server", host: "unknown.example",
|
|
detectedDistribution: unknownDistribution
|
|
)
|
|
let disconnected = Host(
|
|
name: "Disconnected Server", host: "disconnected.example"
|
|
)
|
|
let template = WorkspaceTemplate(
|
|
name: "Operations",
|
|
layout: .split(
|
|
axis: .horizontal,
|
|
fraction: 0.5,
|
|
first: .pane(.ssh(hostID: ubuntu.id)),
|
|
second: .pane(.ssh(hostID: unknown.id))
|
|
)
|
|
)
|
|
let library = HostLibraryModel(
|
|
hosts: [ubuntu, unknown, disconnected], workspaceTemplates: [template]
|
|
)
|
|
library.setStatus(.connected, for: ubuntu.id)
|
|
library.setStatus(.connecting, for: unknown.id)
|
|
let history = ConnectionHistoryModel(
|
|
entries: [
|
|
ConnectionHistoryEntry(
|
|
hostID: ubuntu.id, kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: 20),
|
|
sourceDeviceID: localDeviceID
|
|
),
|
|
ConnectionHistoryEntry(
|
|
hostID: unknown.id, kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: 10),
|
|
sourceDeviceID: localDeviceID
|
|
),
|
|
ConnectionHistoryEntry(
|
|
hostID: disconnected.id, kind: .ssh,
|
|
connectedAt: Date(timeIntervalSince1970: 5),
|
|
sourceDeviceID: localDeviceID
|
|
),
|
|
],
|
|
sourceDeviceID: localDeviceID
|
|
)
|
|
let app = AppModel(hosts: library, connectionHistory: history)
|
|
let navigation = WindowModel()
|
|
let controller = NSHostingController(
|
|
rootView: WelcomeView().environment(app).environment(navigation)
|
|
.environment(\.locale, Locale(identifier: language))
|
|
)
|
|
let window = NSWindow(
|
|
contentRect: CGRect(x: 0, y: 0, width: 680, height: 620),
|
|
styleMask: [.titled, .resizable], backing: .buffered, defer: false
|
|
)
|
|
window.isReleasedWhenClosed = false
|
|
controller.sizingOptions = []
|
|
window.contentViewController = controller
|
|
window.makeKeyAndOrderFront(nil)
|
|
window.setContentSize(CGSize(width: 680, height: 620))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 680, height: 620)
|
|
defer {
|
|
app.shutdownForTermination()
|
|
window.orderOut(nil)
|
|
window.contentViewController = nil
|
|
window.close()
|
|
}
|
|
|
|
#expect(LinuxDistributionIconCatalog.entry(for: ubuntuDistribution) != nil)
|
|
#expect(LinuxDistributionIconCatalog.entry(for: unknownDistribution) == nil)
|
|
|
|
try await Task.sleep(for: .milliseconds(150))
|
|
let field = try #require(
|
|
descendants(controller.view).compactMap { $0 as? NSTextField }.first { $0.isEditable }
|
|
)
|
|
for (appearanceName, appearance) in [
|
|
("light", NSAppearance.Name.aqua),
|
|
("dark", NSAppearance.Name.darkAqua),
|
|
] {
|
|
window.appearance = NSAppearance(named: appearance)
|
|
|
|
try await update(field, with: "", in: controller.view)
|
|
try capture(
|
|
controller.view,
|
|
name: "welcome-glass-recents-\(appearanceName)-\(language)"
|
|
)
|
|
|
|
try await update(field, with: "Server", in: controller.view)
|
|
try capture(
|
|
controller.view,
|
|
name: "welcome-glass-hosts-\(appearanceName)-\(language)"
|
|
)
|
|
|
|
try await update(field, with: "ssh deploy@one-off.example", in: controller.view)
|
|
try capture(
|
|
controller.view,
|
|
name: "welcome-glass-connect-once-\(appearanceName)-\(language)"
|
|
)
|
|
|
|
window.setContentSize(CGSize(width: 360, height: 620))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 360, height: 620)
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
controller.view.layoutSubtreeIfNeeded()
|
|
try capture(
|
|
controller.view,
|
|
name: "welcome-glass-narrow-\(appearanceName)-\(language)"
|
|
)
|
|
window.setContentSize(CGSize(width: 680, height: 620))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 680, height: 620)
|
|
}
|
|
}
|
|
|
|
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
|
|
func welcomePreviewsCommandsAndRejectsRemoteCommands(_ language: String) async throws {
|
|
let library = HostLibraryModel()
|
|
let app = AppModel(hosts: library)
|
|
let navigation = WindowModel()
|
|
let controller = NSHostingController(
|
|
rootView: WelcomeView().environment(app).environment(navigation)
|
|
.environment(\.locale, Locale(identifier: language)))
|
|
let window = NSWindow(
|
|
contentRect: CGRect(x: 0, y: 0, width: 680, height: 540),
|
|
styleMask: [.titled, .resizable], backing: .buffered, defer: false)
|
|
window.isReleasedWhenClosed = false
|
|
controller.sizingOptions = []
|
|
window.contentViewController = controller
|
|
window.makeKeyAndOrderFront(nil)
|
|
window.setContentSize(CGSize(width: 680, height: 540))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 680, height: 540)
|
|
defer {
|
|
app.shutdownForTermination()
|
|
window.orderOut(nil)
|
|
window.contentViewController = nil
|
|
window.close()
|
|
}
|
|
try await Task.sleep(for: .milliseconds(900))
|
|
let field = try #require(descendants(controller.view).compactMap { $0 as? NSTextField }.first { $0.isEditable })
|
|
for (name, query) in [
|
|
("route", "ssh -J bastion@jump.example:2200 -p 2222 deploy@server.example"),
|
|
("unsupported", "ssh -i ~/.ssh/id_ed25519 deploy@server.example"),
|
|
("incomplete", "ssh -p"),
|
|
] {
|
|
field.stringValue = query
|
|
field.delegate?.controlTextDidChange?(
|
|
Notification(name: NSControl.textDidChangeNotification, object: field))
|
|
try await Task.sleep(for: .milliseconds(150))
|
|
controller.view.layoutSubtreeIfNeeded()
|
|
#expect(field.stringValue == query)
|
|
#expect(navigation.workspaces.isEmpty)
|
|
#expect(library.hosts.isEmpty)
|
|
try capture(controller.view, name: "welcome-ssh-\(name)-\(language)")
|
|
if name == "route" {
|
|
window.setContentSize(CGSize(width: 360, height: 540))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 360, height: 540)
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
controller.view.layoutSubtreeIfNeeded()
|
|
try capture(controller.view, name: "welcome-ssh-narrow-\(language)")
|
|
window.setContentSize(CGSize(width: 680, height: 540))
|
|
controller.view.frame = CGRect(x: 0, y: 0, width: 680, height: 540)
|
|
}
|
|
}
|
|
// Return on an invalid command must neither clear the field nor open a session.
|
|
field.stringValue = "ssh server.example uptime"
|
|
field.delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: field))
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
let action = try #require(field.action)
|
|
#expect(field.sendAction(action, to: field.target))
|
|
await Task.yield()
|
|
#expect(navigation.workspaces.isEmpty)
|
|
#expect(field.stringValue == "ssh server.example uptime")
|
|
field.stringValue = "ssh -p2222 deploy@server.example"
|
|
field.delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: field))
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
#expect(field.sendAction(action, to: field.target))
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
let session = try #require(navigation.workspaces.first?.focusedPane.sshSession)
|
|
#expect(session.connectionRoute?.hosts.last?.port == 2222)
|
|
#expect(field.stringValue.isEmpty)
|
|
#expect(library.hosts.isEmpty)
|
|
}
|
|
|
|
private func descendants(_ view: NSView) -> [NSView] {
|
|
[view] + view.subviews.flatMap(descendants)
|
|
}
|
|
|
|
private func update(_ field: NSTextField, with value: String, in view: NSView) async throws {
|
|
field.stringValue = value
|
|
field.delegate?.controlTextDidChange?(
|
|
Notification(name: NSControl.textDidChangeNotification, object: field)
|
|
)
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
view.layoutSubtreeIfNeeded()
|
|
}
|
|
|
|
private func capture(_ view: NSView, name: String) throws {
|
|
guard let directory = ProcessInfo.processInfo.environment["TERMIND_RENDER_DIR"] else { return }
|
|
let url = URL(fileURLWithPath: directory)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
let bitmap = try #require(view.bitmapImageRepForCachingDisplay(in: view.bounds))
|
|
view.cacheDisplay(in: view.bounds, to: bitmap)
|
|
let data = try #require(bitmap.representation(using: .png, properties: [:]))
|
|
try data.write(to: url.appendingPathComponent("\(name).png"))
|
|
}
|
|
}
|