refactor(ui): simplify discovery and custom theme cards
This commit is contained in:
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)
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)])
|
||||
|
||||
Reference in new issue
Block a user