Files
Termind/TFITests/MobileCardRenderingTests.swift
T

231 lines
12 KiB
Swift

import SwiftUI
import TermindApplication
import TermindDomain
import TermindPresentation
import Testing
import UIKit
@testable import TermindIOS
/// Opt-in native screenshots use only in-memory records and never open a connection.
@Suite(.serialized) @MainActor
struct MobileCardRenderingTests {
@Test func renderCardsAndTouchPalette() async throws {
guard ProcessInfo.processInfo.environment["TERMIND_CARD_SCREENSHOTS"] != nil else { return }
let app = try MobileAppModel(inMemory: true)
let group = HostGroup(name: "Production / 生产环境")
app.library.upsert(group)
for (index, color) in [TagColor.teal, .orange, .purple, .none].enumerated() {
let tags =
switch index {
case 0: [String]()
case 1: ["production"]
default: ["production", "ap-east", "long-tag-that-must-stay-on-one-line"]
}
let host = TermindDomain.Host(
name: index == 0 ? "Production Gateway — Shanghai / 生产网关" : "Worker \(index)",
host: "gateway-\(index).production.example.invalid", groupID: group.id,
tags: tags, colorTag: color,
detectedDistribution: index == 3 ? DetectedDistribution(id: "alpine") : nil)
app.library.upsert(host)
if index == 3 {
app.connectionHistory.record(
kind: .ssh, for: host.id,
at: Date.now.addingTimeInterval(-3_600))
}
if index == 3 {
_ = app.knownHosts.save(
KnownHostEntry(
hostname: host.host, fingerprint: "SHA256:0123456789abcdefghijklmnopqrstuvwxyzABCDE"))
}
}
let editableKnownHost = KnownHostEntry(
hostname: "known-host.production.example.invalid", algorithm: .rsa,
fingerprint: "SHA256:editable0123456789abcdefghijklmnopqrstuvwxyz",
addedAt: .now.addingTimeInterval(-86_400))
let hashedAddress =
"|1|" + Data(repeating: 7, count: 20).base64EncodedString()
+ "|" + Data(repeating: 9, count: 20).base64EncodedString()
let hashedKnownHost = KnownHostEntry(
hostname: hashedAddress, algorithm: .ecdsa384,
fingerprint: "SHA256:hashed0123456789abcdefghijklmnopqrstuvwxyz",
addedAt: .now.addingTimeInterval(-604_800))
try app.library.commitSSHImport(SSHImportBatch(knownHosts: [editableKnownHost, hashedKnownHost]))
let key = SSHKeyRef(
label: "Production Key / 生产密钥", algorithm: .rsa,
fingerprint: "SHA256:0123456789abcdefghijklmnopqrstuvwxyzABCDE",
credentialRef: "termind.key.\(UUID())", storage: .thisDevice, keySize: 4096,
hasPassphrase: true, colorTag: .teal)
try app.credentials.upsert(
key, credential: .software(SSHPrivateKeySecret(privateKey: "rendering-fixture")))
try app.library.upsert(
Identity(
name: "Deployment / 部署", username: "deploy", keyID: key.id,
colorTag: .purple),
passwordUpdate: .replace("rendering-fixture"))
_ = app.library.upsert(
Identity(name: "Missing Key / 密钥缺失", username: "missing", keyID: UUID(), colorTag: .orange))
_ = app.library.upsert(
Identity(name: "No Secret / 无凭据", username: "unfinished"))
try app.credentials.upsert(
SSHKeyRef(
label: "Missing Private Key / 私钥缺失", algorithm: .ecdsa384,
fingerprint: "SHA256:missing", credentialRef: "termind.key.\(UUID())", storage: .iCloud))
let snippetPackage = SnippetPackage(name: "Operations / 运维")
app.snippets.upsert(snippetPackage)
app.snippets.upsert(
Snippet(
title: "Inspect disk usage / 检查磁盘", body: "df -h\ndu -sh /var/log",
packageID: snippetPackage.id))
let server = ServerMetric(selection: MetricsSelection(id: UUID()), name: "Production Gateway")
server.isConnecting = false
server.status = ""
server.point = MetricsPoint(
date: .now, cpu: 42, memory: 67, diskRead: 125_000, diskWrite: 8_000, networkReceive: 400_000,
networkSend: 9_000,
networkDevices: [
MetricsDevicePoint(
id: "en0", read: 400_000, write: 9_000, readTotal: 8_000_000, writeTotal: 1_200_000,
kind: .hardware, addresses: ["192.0.2.10"]),
MetricsDevicePoint(
id: "utun4", read: 12_000, write: 4_000, readTotal: 900_000, writeTotal: 300_000,
kind: .virtual, addresses: ["10.0.0.2"]),
])
for language in ["en", "zh-Hans", "zh-Hant"] {
for scheme in [ColorScheme.light, .dark] {
try await capture("vault", language: language, scheme: scheme) {
NavigationStack {
MobileVaultView(
app: app, editingHost: .constant(nil), activateHost: { _ in },
open: { _, _ in })
}
}
try await capture("history", language: language, scheme: scheme) {
NavigationStack {
MobileConnectionHistoryView(app: app, open: { _, _ in })
}
}
try await capture("workspaces-recents", language: language, scheme: scheme) {
NavigationStack {
MobileWorkspaceListView(app: app, scene: MobileSceneModel(app: app))
}
}
try await capture("snippets", language: language, scheme: scheme) {
NavigationStack { MobileSnippetsView(app: app) }
}
try await capture("snippets-search", language: language, scheme: scheme) {
NavigationStack { MobileSnippetsView(app: app, initialQuery: "Inspect") }
}
try await capture("keychain", language: language, scheme: scheme) {
NavigationStack { MobileCredentialsView(app: app) }
}
try await capture("settings", language: language, scheme: scheme) {
NavigationStack { MobileSettingsView(app: app) }
}
try await capture("palette", language: language, scheme: scheme) {
NavigationStack {
Form {
Section("Color") { ColorTagPicker(selection: .constant(.teal), layout: .touch) }
Section("Color") { ColorTagPicker(selection: .constant(.none), layout: .touch) }
}.navigationTitle("Host")
}
}
try await capture("metrics", language: language, scheme: scheme) {
NavigationStack {
MobileCardPage {
MobileActionCard(action: {}) {
VStack(alignment: .leading, spacing: 12) {
Text(server.name).font(.subheadline.bold()).lineLimit(1)
MetricsInstrumentBody(server: server, sleeping: false)
}
} actions: {
Button("Pause") {}
}
}.navigationTitle("Metrics")
}
}
try await capture("metrics-details", language: language, scheme: scheme) {
NavigationStack {
ScrollView { MetricsSystemPanels(server: server).padding() }
.background(Color(uiColor: .systemGroupedBackground))
.navigationTitle(server.name)
}
}
try await capture("known-hosts", language: language, scheme: scheme) {
NavigationStack { MobileKnownHostsView(app: app) }
}
try await capture("known-host-editor", language: language, scheme: scheme) {
MobileKnownHostEditor(app: app, entry: editableKnownHost)
}
try await capture("known-host-editor-hashed", language: language, scheme: scheme) {
MobileKnownHostEditor(app: app, entry: hashedKnownHost)
}
}
try await capture("large-type", language: language, scheme: .light) {
NavigationStack {
MobileVaultView(
app: app, editingHost: .constant(nil), activateHost: { _ in },
open: { _, _ in })
}
.environment(\.dynamicTypeSize, .accessibility3)
}
try await capture("metrics-large-type", language: language, scheme: .light) {
MobileCardPage {
MobileCard { MetricsInstrumentBody(server: server, sleeping: false, layout: .adaptive) }
}.environment(\.dynamicTypeSize, .accessibility3)
}
try await capture("key-editor", language: language, scheme: .light) {
MobileKeyEditor(
model: MobileKeyEditorModel(mode: .edit, credentials: app.credentials, key: app.credentials.keys[0])
)
}
try await capture("empty", language: language, scheme: .light) {
NavigationStack {
MobileVaultView(
app: try! MobileAppModel(inMemory: true), editingHost: .constant(nil),
activateHost: { _ in },
open: { _, _ in })
}
}
try await capture("known-hosts-large-type", language: language, scheme: .light) {
NavigationStack { MobileKnownHostsView(app: app) }
.environment(\.dynamicTypeSize, .accessibility3)
}
}
}
private func capture<V: View>(
_ name: String, language: String, scheme: ColorScheme, @ViewBuilder view: () -> V
) async throws {
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
let previous = scene.keyWindow
let window = UIWindow(windowScene: scene)
let controller = UIHostingController(
rootView: view().environment(\.locale, Locale(identifier: language)).preferredColorScheme(scheme))
window.rootViewController = controller
if name == "opaque" { controller.traitOverrides.accessibilityContrast = .high }
window.windowLevel = .alert + 1
window.makeKeyAndVisible()
defer {
window.isHidden = true
window.rootViewController = nil
previous?.makeKey()
}
controller.view.layoutIfNeeded()
try await Task.sleep(for: .milliseconds(500))
let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
let data = try #require(image.pngData())
#expect(data.count > 10000)
let directory = FileManager.default.temporaryDirectory.appending(path: "card-screenshots")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
let appearance = scheme == .dark ? "dark" : "light"
let output = directory.appending(path: "\(device)-\(language)-\(name)-\(appearance).png")
try data.write(to: output)
print("Card snapshot: \(output.path)")
}
}