refactor(ui): simplify discovery and custom theme cards

This commit is contained in:
Kaede Akino committed 2026-10-16 23:45:08 +08:00
1 parent a4f0a5622f
commit b24e303725
18 files changed
+334 -99

No files matched your search

@@ -57,15 +57,15 @@ public struct HostProtocolProfileControls: View {
public struct HostConnectionFields: View {
@Binding private var host: Host
private let connectionProtocol: HostTerminalProtocol
private let discover: (() -> Void)?
public init(host: Binding<Host>, protocol connectionProtocol: HostTerminalProtocol) {
public init(
host: Binding<Host>, protocol connectionProtocol: HostTerminalProtocol = .ssh,
discover: (() -> Void)? = nil
) {
_host = host
self.connectionProtocol = connectionProtocol
}
public init(host: Binding<Host>) {
_host = host
connectionProtocol = .ssh
self.discover = discover
}
public var body: some View {
@@ -94,6 +94,13 @@ public struct HostConnectionFields: View {
#endif
}
}
if let discover, !host.isTailscaleManaged,
LocalDiscoveryProtocol(terminalProtocol: connectionProtocol) != nil
{
Button(
AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "dot.radiowaves.left.and.right",
action: discover)
}
}
}
@@ -27,7 +27,9 @@ public struct HostWakeOnLANSection: View {
.textInputAutocapitalization(.never).autocorrectionDisabled().keyboardType(.asciiCapable)
#endif
if let fetchMAC {
Button(AppStrings.LocalNetwork.fetchMAC.swiftUIKey, action: fetchMAC)
Button(
AppStrings.LocalNetwork.fetchMAC.swiftUIKey, systemImage: "rectangle.connected.to.line.below",
action: fetchMAC)
}
Toggle(
AppStrings.LocalNetwork.autoWake.swiftUIKey,
@@ -21,7 +21,7 @@ public struct LocalDiscoveryView: View {
}.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if model.hosts.isEmpty {
ContentUnavailableView {
Label(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network")
Label(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "dot.radiowaves.left.and.right")
} description: {
Text(status)
}
@@ -33,16 +33,6 @@ public struct LocalDiscoveryView: View {
VStack(alignment: .leading, spacing: 6) {
Text(host.name).font(.headline)
Text("\(host.address):\(host.port)").font(.callout.monospaced())
Text(
host.connectionProtocol == .telnet
? AppStrings.LocalNetwork.telnetPortOpen
: host.source == .bonjour
? AppStrings.LocalNetwork.bonjour : AppStrings.LocalNetwork.verifiedSSH
).font(.caption).foregroundStyle(.secondary)
if host.source == .bonjour {
Text(AppStrings.LocalNetwork.advertisedSSH).font(.caption).foregroundStyle(
.secondary)
}
}.padding(.vertical, 4)
}.buttonStyle(.plain)
}
@@ -3,11 +3,9 @@ import TermindDomain
public struct TerminalCustomThemeSummary: View {
private let theme: TerminalCustomTheme
private let library: TerminalThemeLibrary
public init(theme: TerminalCustomTheme, library: TerminalThemeLibrary) {
public init(theme: TerminalCustomTheme) {
self.theme = theme
self.library = library
}
public var body: some View {
@@ -17,15 +15,6 @@ public struct TerminalCustomThemeSummary: View {
VStack(alignment: .leading, spacing: 6) {
Text(verbatim: theme.name).font(.headline)
Text(theme.appearance.title).font(.caption).foregroundStyle(.secondary)
ForEach([false, true], id: \.self) { isDark in
if library.selectedTheme(isDark: isDark)?.id == theme.id {
Label(
(isDark ? AppStrings.Settings.darkTheme : AppStrings.Settings.lightTheme).swiftUIKey,
systemImage: "checkmark"
)
.font(.caption).foregroundStyle(.tint)
}
}
}
Spacer(minLength: 0)
}
+3 -10
View File
@@ -31,9 +31,8 @@ struct MobileHostEditor: View {
@State private var didLoadCustom = false
@State private var selectedProtocol: HostTerminalProtocol
init(app: MobileAppModel, host: Host, discover: Bool = false) {
init(app: MobileAppModel, host: Host) {
self.app = app
_showsDiscovery = State(initialValue: discover)
original = host
_draft = State(initialValue: host)
_selectedProtocol = State(initialValue: host.sshEnabled ? .ssh : .telnet)
@@ -92,14 +91,8 @@ struct MobileHostEditor: View {
MobileTagField(tags: $draft.tags, suggestions: library.allTags)
}
}
HostConnectionFields(host: $draft, protocol: selectedProtocol).disabled(draft.isTailscaleManaged)
if !draft.isTailscaleManaged, LocalDiscoveryProtocol(terminalProtocol: selectedProtocol) != nil {
Section {
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
showsDiscovery = true
}
}
}
HostConnectionFields(host: $draft, protocol: selectedProtocol, discover: { showsDiscovery = true })
.disabled(draft.isTailscaleManaged)
if selectedProtocol == .ssh && draft.sshEnabled {
if draft.isTailscaleManaged {
Section(AppStrings.Vault.authentication.swiftUIKey) {
@@ -17,11 +17,14 @@ struct MobileLocalDiscoveryView: View {
.navigationTitle(AppStrings.LocalNetwork.discovery.swiftUIKey)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(AppStrings.Common.cancel.swiftUIKey) { dismiss() }
Button(AppStrings.Common.cancel.swiftUIKey, systemImage: "xmark") { dismiss() }
.labelStyle(.iconOnly)
}
ToolbarItem(placement: .primaryAction) {
Button(AppStrings.LocalNetwork.rescan.swiftUIKey) { model.scan() }
ToolbarItem(placement: .confirmationAction) {
Button(AppStrings.LocalNetwork.rescan.swiftUIKey, systemImage: "arrow.clockwise") { model.scan() }
.labelStyle(.iconOnly)
}
.mobileProminentAction()
}
}
}
@@ -28,21 +28,10 @@ struct MobileCustomThemesView: View {
MobileActionCard(alignment: .leading) {
draft = .init(theme, isNew: false)
} content: {
TerminalCustomThemeSummary(theme: theme, library: model.themeLibrary)
.padding(.trailing, 32)
TerminalCustomThemeSummary(theme: theme)
} actions: {
actions(theme)
}
.overlay(alignment: .topTrailing) {
Menu {
actions(theme)
} label: {
Label(AppStrings.Common.edit.swiftUIKey, systemImage: "ellipsis")
.labelStyle(.iconOnly)
.frame(width: 44, height: 44).contentShape(.rect)
}
.padding(4)
}
.operationConfirmation(
.init(AppStrings.Settings.deleteCustomThemeConfirmation),
isPresented: $deleting.isPresenting { $0.id == theme.id },
+2 -15
View File
@@ -300,7 +300,6 @@ struct MobileVaultView: View {
let executeSnippet: (Snippet, [Host.ID]) -> Void
private let openMetrics: (() -> Void)?
@Binding var editingHost: Host?
@State private var discoverNewHost = false
@State private var wakeNotice: String?
@State private var query = ""
@State private var tailscaleHost: Host?
@@ -505,10 +504,6 @@ struct MobileVaultView: View {
Button(AppStrings.Vault.newHost.swiftUIKey, systemImage: "server.rack") {
editingHost = Host(name: "", host: "")
}
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
discoverNewHost = true
editingHost = Host(name: "", host: "")
}
Button(AppStrings.Vault.newGroup.swiftUIKey, systemImage: "folder.badge.plus") {
groupEditorRequest = GroupEditorRequest(groupID: nil, parentID: nil)
}
@@ -520,8 +515,8 @@ struct MobileVaultView: View {
.sheet(item: $tailscaleHost) { host in
TailscalePeerView(model: app.tailscale, host: host) { UIPasteboard.general.string = $0 }
}
.sheet(item: $editingHost, onDismiss: { discoverNewHost = false }) {
MobileHostEditor(app: app, host: $0, discover: discoverNewHost)
.sheet(item: $editingHost) {
MobileHostEditor(app: app, host: $0)
}
.alert(
AppStrings.LocalNetwork.wake,
@@ -700,10 +695,6 @@ struct MobileVaultView: View {
Button(AppStrings.Vault.newHost.swiftUIKey, systemImage: "server.rack") {
editingHost = Host(name: "", host: "", groupID: group.id)
}
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
discoverNewHost = true
editingHost = Host(name: "", host: "", groupID: group.id)
}
Button(AppStrings.Vault.newSubgroup.swiftUIKey, systemImage: "folder.badge.plus") {
groupEditorRequest = GroupEditorRequest(groupID: nil, parentID: group.id)
}
@@ -736,10 +727,6 @@ struct MobileVaultView: View {
Button(AppStrings.Vault.newHost.swiftUIKey, systemImage: "server.rack") {
editingHost = Host(name: "", host: "")
}
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
discoverNewHost = true
editingHost = Host(name: "", host: "")
}
Button(AppStrings.Vault.newGroup.swiftUIKey, systemImage: "folder.badge.plus") {
groupEditorRequest = GroupEditorRequest(groupID: nil, parentID: nil)
}
+21
View File
@@ -78,6 +78,7 @@ struct MobileTerminalThemeTests {
model.saveCustomTheme(
.init(name: "Daylight", appearance: .light, baseThemeName: "Termind Light"), isNew: true)
model.selectCustomTheme(id: theme.id, isDark: true)
model.selectCustomTheme(id: theme.id, isDark: false)
let initial = model.themeLibrary
let draft = TerminalCustomThemeDraft(theme, isNew: false)
let content = Group {
@@ -113,6 +114,12 @@ struct MobileTerminalThemeTests {
search.searchBar.placeholder
== AppLocalization.resolve(AppStrings.Settings.searchThemes, locale: Locale(identifier: language)))
#expect(navigation.topViewController?.navigationItem.hidesSearchBarWhenScrolling == false)
let labels = accessibilityLabels(in: window)
#expect(labels.contains { $0.contains(theme.name) })
for resource in [AppStrings.Settings.lightTheme, AppStrings.Settings.darkTheme, AppStrings.Common.edit] {
let unwanted = AppLocalization.resolve(resource, locale: Locale(identifier: language))
#expect(!labels.contains { $0.contains(unwanted) })
}
}
let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
@@ -176,6 +183,20 @@ struct MobileTerminalThemeTests {
return controller.children.lazy.compactMap { navigationController(in: $0) }.first
}
private func accessibilityLabels(in root: NSObject) -> [String] {
var seen = Set<ObjectIdentifier>()
func visit(_ element: NSObject) -> [String] {
guard seen.insert(ObjectIdentifier(element)).inserted else { return [] }
var children: [NSObject] = (element as? UIView)?.subviews ?? []
let count = element.accessibilityElementCount()
if count != NSNotFound, count > 0 {
children += (0..<count).compactMap { element.accessibilityElement(at: $0) as? NSObject }
}
return [element.accessibilityLabel].compactMap { $0 } + children.flatMap(visit)
}
return visit(root)
}
private func scrollViews(in view: UIView) -> [UIScrollView] {
let scroll = (view as? UIScrollView).map { [$0] } ?? []
return scroll + view.subviews.flatMap { scrollViews(in: $0) }
@@ -0,0 +1,153 @@
import SwiftUI
import TermindApplication
import TermindDomain
import TermindPresentation
import Testing
import UIKit
@testable import TermindIOS
@Suite(.requiresKeyWindow, .serialized) @MainActor
struct MobileLocalDiscoveryRenderingTests {
@Test(arguments: [HostTerminalProtocol.ssh, .telnet, .mosh, .eternalTerminal], [false, true])
func connectionActionsRespectProtocolAndManagedHosts(
connectionProtocol: HostTerminalProtocol, managed: Bool
) async throws {
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
let previous = scene.keyWindow
var host = TermindDomain.Host(name: "Example", host: "192.168.1.24")
if managed { host.tailscale = .init(groupID: UUID(), stableNodeID: "preview") }
let window = UIWindow(windowScene: scene)
window.overrideUserInterfaceStyle = .light
let controller = UIHostingController(
rootView: NavigationStack {
Form {
HostConnectionFields(host: .constant(host), protocol: connectionProtocol, discover: {})
HostWakeOnLANSection(
preferences: .constant(.init()), requestAccess: { true }, fetchMAC: {})
}
.navigationTitle(AppStrings.Vault.addHost.swiftUIKey)
}
.environment(\.locale, Locale(identifier: "en")))
window.rootViewController = controller
window.makeKeyAndVisible()
defer {
window.isHidden = true
window.rootViewController = nil
previous?.makeKey()
}
try await Task.sleep(for: .milliseconds(500))
let labels = accessibilityLabels(in: window)
let discovery = AppLocalization.resolve(AppStrings.LocalNetwork.discovery, locale: Locale(identifier: "en"))
let fetchMAC = AppLocalization.resolve(AppStrings.LocalNetwork.fetchMAC, locale: Locale(identifier: "en"))
let supportsDiscovery = !managed && [.ssh, .telnet].contains(connectionProtocol)
#expect(labels.contains { $0.contains(discovery) } == supportsDiscovery)
#expect(labels.contains { $0.contains(fetchMAC) })
if !managed, connectionProtocol == .ssh {
let data = UIGraphicsImageRenderer(bounds: window.bounds).pngData { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
Attachment.record(data, named: "iphone-connection-actions.png")
}
}
@Test(arguments: ["en", "zh-Hans"])
func discoveredHostsUseTheNativeSheet(language: String) async throws {
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
let previous = scene.keyWindow
let service = PreviewDiscovery()
let model = LocalDiscoveryModel(service: service)
let window = UIWindow(windowScene: scene)
window.overrideUserInterfaceStyle = .light
let controller = UIHostingController(
rootView: DiscoverySheetPreview(model: model)
.environment(\.locale, Locale(identifier: language)))
window.rootViewController = controller
window.makeKeyAndVisible()
defer {
model.cancel()
window.isHidden = true
window.rootViewController = nil
previous?.makeKey()
}
for _ in 0..<100 {
if model.phase == .finished { break }
try await Task.sleep(for: .milliseconds(20))
}
try #require(model.phase == .finished)
#expect(model.hosts.contains { $0.source == .bonjour })
#expect(model.hosts.contains { $0.source == .probe })
#expect(service.scanCount == 1)
try #require(controller.presentedViewController != nil)
model.scan()
for _ in 0..<100 {
if model.phase == .finished { break }
try await Task.sleep(for: .milliseconds(20))
}
try #require(model.phase == .finished)
#expect(service.scanCount == 2)
try await Task.sleep(for: .milliseconds(700))
window.layoutIfNeeded()
let labels = accessibilityLabels(in: window)
#expect(labels.contains { $0.contains("192.168.1.24:22") })
for resource in [
AppStrings.LocalNetwork.bonjour, AppStrings.LocalNetwork.advertisedSSH,
AppStrings.LocalNetwork.verifiedSSH, AppStrings.LocalNetwork.telnetPortOpen,
] {
let unwanted = AppLocalization.resolve(resource, locale: Locale(identifier: language))
#expect(!labels.contains { $0.contains(unwanted) })
}
let data = UIGraphicsImageRenderer(bounds: window.bounds).pngData { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
let name = "iphone-\(language)-discovery.png"
Attachment.record(data, named: name)
let directory = FileManager.default.temporaryDirectory.appending(path: "local-discovery-screenshots")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let output = directory.appending(path: name)
try data.write(to: output)
print("Local discovery screenshot: \(output.path)")
}
private struct DiscoverySheetPreview: View {
let model: LocalDiscoveryModel
@State private var presented = false
var body: some View {
Color(uiColor: .systemGroupedBackground)
.sheet(isPresented: $presented) {
MobileLocalDiscoveryView(model: model, select: { _ in })
}
.onAppear { presented = true }
}
}
private func accessibilityLabels(in root: NSObject) -> [String] {
var seen = Set<ObjectIdentifier>()
func visit(_ element: NSObject) -> [String] {
guard seen.insert(ObjectIdentifier(element)).inserted else { return [] }
var children: [NSObject] = (element as? UIView)?.subviews ?? []
let count = element.accessibilityElementCount()
if count != NSNotFound, count > 0 {
children += (0..<count).compactMap { element.accessibilityElement(at: $0) as? NSObject }
}
return [element.accessibilityLabel].compactMap { $0 } + children.flatMap(visit)
}
return visit(root)
}
}
@MainActor
private final class PreviewDiscovery: LocalHostDiscovering {
let connectionProtocol = LocalDiscoveryProtocol.ssh
private(set) var scanCount = 0
func discover() async throws -> [DiscoveredLocalHost] {
scanCount += 1
return [
.init(name: "Mac mini", address: "mac-mini.local", port: 22, source: .bonjour),
.init(name: "192.168.1.24", address: "192.168.1.24", port: 22, source: .probe),
.init(name: "Raspberry Pi", address: "raspberrypi.local", port: 22, source: .bonjour),
]
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ struct TermindCommands: Commands {
window?.openHostEditor(host: nil)
}
.termindShortcut("n", modifiers: [.command, .shift])
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "dot.radiowaves.left.and.right") {
window?.openVaultWork(.hostEditor(nil, discover: true), on: .hosts)
}
.disabled(window == nil)
+2 -9
View File
@@ -159,15 +159,8 @@ struct HostEditorView: View {
}
}
HostConnectionFields(host: $draft, protocol: selectedProtocol).disabled(draft.isTailscaleManaged)
if !draft.isTailscaleManaged, LocalDiscoveryProtocol(terminalProtocol: selectedProtocol) != nil {
Section {
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
openDiscovery()
}
}
}
HostConnectionFields(host: $draft, protocol: selectedProtocol, discover: openDiscovery)
.disabled(draft.isTailscaleManaged)
if selectedProtocol == .ssh && draft.sshEnabled {
if draft.isTailscaleManaged {
@@ -10,7 +10,7 @@ struct HostLocalDiscoveryView: View {
var body: some View {
VaultDetailScaffold(
title: AppStrings.LocalNetwork.discovery, symbol: "network", close: back,
title: AppStrings.LocalNetwork.discovery, symbol: "dot.radiowaves.left.and.right", close: back,
closeLabel: AppStrings.Common.back, closeSymbol: "chevron.backward"
) {
LocalDiscoveryView(model: model, select: select)
@@ -10,7 +10,7 @@ struct HostMACAddressView: View {
var body: some View {
VaultDetailScaffold(
title: AppStrings.LocalNetwork.fetchMAC, symbol: "network", close: back,
title: AppStrings.LocalNetwork.fetchMAC, symbol: "rectangle.connected.to.line.below", close: back,
closeLabel: AppStrings.Common.back, closeSymbol: "chevron.backward"
) {
VStack(spacing: 12) {
+3 -3
View File
@@ -162,7 +162,7 @@ struct HostsPageView: ManagedCardPage {
Button(AppStrings.Vault.newHost.swiftUIKey, systemImage: MenuSymbol.newHost) {
openHostEditor(nil, groupID: group.id)
}
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "dot.radiowaves.left.and.right") {
openWork(.hostEditor(nil, groupID: group.id, discover: true))
}
Button(AppStrings.Vault.newSubgroup.swiftUIKey, systemImage: MenuSymbol.newGroup) {
@@ -186,7 +186,7 @@ struct HostsPageView: ManagedCardPage {
Button(AppStrings.Vault.newHost.swiftUIKey, systemImage: MenuSymbol.newHost) {
openHostEditor(nil)
}
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "network") {
Button(AppStrings.LocalNetwork.discovery.swiftUIKey, systemImage: "dot.radiowaves.left.and.right") {
openWork(.hostEditor(nil, discover: true))
}
Button(AppStrings.Vault.newGroup.swiftUIKey, systemImage: MenuSymbol.newGroup) {
@@ -230,7 +230,7 @@ struct HostsPageView: ManagedCardPage {
NativeMenuEntry(title: AppStrings.Vault.newHost, symbol: MenuSymbol.newHost) {
openHostEditor(nil, groupID: groupID)
},
NativeMenuEntry(title: AppStrings.LocalNetwork.discovery, symbol: "network") {
NativeMenuEntry(title: AppStrings.LocalNetwork.discovery, symbol: "dot.radiowaves.left.and.right") {
openWork(.hostEditor(nil, groupID: groupID, discover: true))
},
NativeMenuEntry(title: AppStrings.Vault.newGroup, symbol: MenuSymbol.newGroup) {
@@ -26,21 +26,9 @@ struct TerminalCustomThemesPage: View {
CardGrid {
ForEach(app.terminalSettings.themeLibrary.themes(matching: window.customThemeQuery)) { theme in
CardTile(activate: { edit(theme) }, activatesOnReturn: true, activatesOnSingleClick: true) {
TerminalCustomThemeSummary(theme: theme, library: app.terminalSettings.themeLibrary)
.padding(.trailing, 24)
TerminalCustomThemeSummary(theme: theme)
}
.contextMenu { actions(theme) }
.overlay(alignment: .topTrailing) {
Menu {
actions(theme)
} label: {
Label(AppStrings.Common.edit.swiftUIKey, systemImage: "ellipsis").labelStyle(
.iconOnly)
}
.menuStyle(.borderlessButton)
.fixedSize()
.padding(10)
}
}
}
}
@@ -0,0 +1,79 @@
import AppKit
import SwiftUI
import TermindApplication
import TermindDomain
import Testing
@testable import Termind
@Suite(.requiresVisibleWindow, .serialized) @MainActor
struct LocalDiscoveryRenderingTests {
@Test(arguments: ["en", "zh-Hans"])
func discoveredHostsUseTheNativeDetail(language: String) async throws {
let service = PreviewDiscovery()
let model = LocalDiscoveryModel(service: service)
let controller = NSHostingController(
rootView: HostLocalDiscoveryView(model: model, back: {}, select: { _ in })
.environment(\.locale, Locale(identifier: language))
.environment(\.colorScheme, .light)
.frame(width: 720, height: 520))
controller.sizingOptions = []
let window = NSWindow(
contentRect: CGRect(x: 100, y: 100, width: 720, height: 520),
styleMask: [.titled, .closable], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.appearance = NSAppearance(named: .aqua)
window.title = "Termind"
window.contentViewController = controller
window.center()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
defer {
model.cancel()
window.orderOut(nil)
window.contentViewController = nil
window.close()
}
for _ in 0..<100 {
if model.phase == .finished { break }
try await Task.sleep(for: .milliseconds(20))
}
try #require(model.phase == .finished)
#expect(model.hosts.contains { $0.source == .bonjour })
#expect(model.hosts.contains { $0.source == .probe })
#expect(service.scanCount == 1)
model.scan()
for _ in 0..<100 {
if model.phase == .finished { break }
try await Task.sleep(for: .milliseconds(20))
}
try #require(model.phase == .finished)
#expect(service.scanCount == 2)
controller.view.layoutSubtreeIfNeeded()
try await Task.sleep(for: .milliseconds(700))
let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds))
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
Attachment.record(
try #require(bitmap.representation(using: .png, properties: [:])),
named: "macos-\(language)-discovery.png")
// An external runner can capture compositor output, including native glass, during this interval.
FileHandle.standardOutput.write(
Data("TERMIND_DISCOVERY_WINDOW|\(window.windowNumber)|macos-\(language)-discovery\n".utf8))
try await Task.sleep(for: .seconds(3))
}
}
@MainActor
private final class PreviewDiscovery: LocalHostDiscovering {
let connectionProtocol = LocalDiscoveryProtocol.ssh
private(set) var scanCount = 0
func discover() async throws -> [DiscoveredLocalHost] {
scanCount += 1
return [
.init(name: "Mac mini", address: "mac-mini.local", port: 22, source: .bonjour),
.init(name: "192.168.1.24", address: "192.168.1.24", port: 22, source: .probe),
.init(name: "Raspberry Pi", address: "raspberrypi.local", port: 22, source: .bonjour),
]
}
}
@@ -7,9 +7,50 @@ import Testing
@testable import Termind
/// Offscreen hosting leaves the user's windows and keyboard focus untouched.
/// Native rendering; tests that show windows opt in separately.
@Suite(.serialized) @MainActor
struct TerminalThemePageRenderingTests {
@Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans"])
func selectedCustomThemeCardsRenderWithoutMenuButtons(language: String) async throws {
let model = TerminalSettingsModel()
let theme = TerminalCustomTheme(name: "Ocean", appearance: .both, baseThemeName: "Termind Light")
model.saveCustomTheme(theme, isNew: true)
model.selectCustomTheme(id: theme.id, isDark: false)
model.selectCustomTheme(id: theme.id, isDark: true)
let initial = model.themeLibrary
let app = AppModel(hosts: HostLibraryModel(), terminalSettings: model)
defer { app.shutdownForTermination() }
let navigation = WindowModel(workspaces: [])
navigation.openSettings(route: .terminalCustomThemes)
let controller = NSHostingController(
rootView: TerminalCustomThemesPage().environment(app).environment(navigation)
.environment(\.locale, Locale(identifier: language))
.frame(width: 720, height: 520))
controller.sizingOptions = []
let window = NSWindow(
contentRect: CGRect(x: 100, y: 100, width: 720, height: 520),
styleMask: [.titled, .closable], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.appearance = NSAppearance(named: .aqua)
window.contentViewController = controller
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
defer {
window.orderOut(nil)
window.contentViewController = nil
window.close()
}
try await Task.sleep(for: .milliseconds(700))
controller.view.layoutSubtreeIfNeeded()
#expect(descendants(in: controller.view, ofType: NSPopUpButton.self).isEmpty)
let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds))
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
Attachment.record(
try #require(bitmap.representation(using: .png, properties: [:])),
named: "macos-custom-library-\(language).png")
#expect(model.themeLibrary == initial)
}
@Test(
.enabled(if: ProcessInfo.processInfo.environment["TERMIND_THEME_SCREENSHOTS"] == "1"),
arguments: ["en", "zh-Hans"], [CGFloat(650), CGFloat(1000)])