Fix edit sidebar size

This commit is contained in:
Kaede Akino committed 2026-09-19 11:31:06 +08:00
1 parent 509484ae4c
commit 9ef2c9c8cc
14 files changed
+193 -147

No files matched your search

+17 -16
View File
@@ -7295,6 +7295,23 @@
}
}
},
"Default" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "默认"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "預設"
}
}
}
},
"Default Network Interfaces" : {
"localizations" : {
"zh-Hans" : {
@@ -10707,22 +10724,6 @@
}
}
},
"Follow Global Settings" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "跟随全局设置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "跟隨全域設定"
}
}
}
},
"Follow Permissions" : {
"extractionState" : "stale",
"localizations" : {
@@ -35,13 +35,15 @@ public struct ConnectionJourneyHeader: View {
}
VStack(alignment: .leading, spacing: 3) {
Text(journey.destination?.name ?? String(localized: "SSH Connection"))
Text(title)
.font(.title3.weight(.semibold))
.lineLimit(1)
.help(title)
Text(detail)
.font(.callout.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.help(detail)
}
Spacer(minLength: 6)
@@ -63,6 +65,8 @@ public struct ConnectionJourneyHeader: View {
.padding(.horizontal, isEmbedded ? 16 : 34)
}
private var title: String { journey.destination?.name ?? String(localized: "SSH Connection") }
private var detail: String {
let protocolName: String =
switch journey.purpose {
@@ -1,33 +1,20 @@
import SwiftUI
import TermindDomain
public struct HostIntegrationSection<Selection: View>: View {
private let selection: Selection
public init(@ViewBuilder selection: () -> Selection) { self.selection = selection() }
public var body: some View {
Section("Integration") {
VStack(alignment: .leading, spacing: 8) {
Text("Shell Integration")
selection.frame(maxWidth: .infinity)
}
}
}
}
public struct HostShellIntegrationPicker: View {
public struct HostIntegrationSection: View {
@Binding private var mode: HostShellIntegrationMode
public init(mode: Binding<HostShellIntegrationMode>) { _mode = mode }
public var body: some View {
Picker("Shell Integration", selection: $mode) {
Text("Enable").tag(HostShellIntegrationMode.enabled)
Text("Follow Global Settings").tag(HostShellIntegrationMode.followGlobalSettings)
Text("Disable").tag(HostShellIntegrationMode.disabled)
Section("Integration") {
Picker("Shell Integration", selection: $mode) {
Text("Default").tag(HostShellIntegrationMode.followGlobalSettings)
Text("Enable").tag(HostShellIntegrationMode.enabled)
Text("Disable").tag(HostShellIntegrationMode.disabled)
}
.pickerStyle(.menu)
.menuOrder(.fixed)
}
.labelsHidden()
.pickerStyle(.segmented)
}
}
+12 -6
View File
@@ -87,14 +87,19 @@ the latest record counts as unchanged.
Snippets, Known Hosts, Welcome and editor routes. Settings data is app-wide; window-local Settings
navigation remembers and restores the replaced destination.
- Desktop detail minimum is 360 points; Assistant reading maximum is independently 840. Opening
Navigator grows the window as needed. Vault reserves a 280-point list and 360–400-point editor,
overlaying below 641 points; covered lists retain their last visible size. Editor/space transitions
take 0.22 seconds, preserve content size and respect Reduce Motion. Metrics below 700 points uses
Navigator grows the window as needed. Vault reserves a 280-point list, one-point divider and fixed
320-point editor (including the embedded forwarding Journey), overlaying below 601 content points;
the full-page editor fills available width and covered lists retain their last visible size. Editor/space
transitions take 0.22 seconds, preserve content size and respect Reduce Motion. Metrics below 700 points uses
a titlebar Status/Processes picker with hidden title; wider layouts show both panels/title.
Covered panels retain size, table identity, sorting and scroll. Selection/width are window-local;
changing server selects Status, while focusing search or narrowing during search reveals Processes.
Compact Pause/Resume goes in More; macOS 15 search uses a toolbar popover. Resizing changes layout,
never routes, editor identity, Workspace composition or monitoring ownership.
Vault grouped forms retain native horizontal insets without expanding their scroll content; only
vertical scrolling is allowed. The embedded Journey retains 16-point horizontal insets and a horizontal
route scroller. Host integration uses a native labeled menu on both platforms, fitting narrow editors
without widening them or clipping their footers.
- `AssistantLibraryModel` owns standalone conversations, never Workspace panes. Workspaces own
SSH/SFTP/serial panes and session-scoped conversations; sessions own live transports and
terminal/browser state. Assistant ordering/pinning is metadata-only during runs, approvals or
@@ -251,9 +256,10 @@ interactive input capture, retaining bracketed paste/broadcast. Remote mouse cap
keep Ghostty handling; mouse reports stay outside broadcast capture. Empty paste arms no deferred capture;
bracketed-paste classification spans chunks through its terminator or existing short-lived ticket expiry.
Host Shell Integration is Vault-synced Follow Global Settings/Enable/Disable; V22 adds a column while
freezing V1–V21 Hosts and defaults legacy data to Follow Global. Both explicit-save editors place it after
Group (desktop Group & Tags), with Follow Global centered between Enable/Disable. Each SSH attempt resolves
Host Shell Integration is Vault-synced Default/Enable/Disable; V22 adds a column while
freezing V1–V21 Hosts and defaults legacy data to follow global settings. Both explicit-save editors place it after
Group (desktop Group & Tags), with a native menu ordered Default, Enable, Disable. Default follows the global
preference without changing persisted values. Each SSH attempt resolves
destination override/global preference once before authentication for bootstrap, History, Command Generator,
system info and directory actions. Live edits never inject scripts/reconnect/change that attempt; new/manual/
automatic reconnects resolve again. SFTP/forwarding route identity is unchanged.
@@ -90,7 +90,7 @@ struct MobileHostEditor: View {
ForEach(library.groups) { Text($0.name).tag(Optional($0.id)) }
}
}
HostIntegrationSection { MobileHostIntegrationPicker(mode: $draft.shellIntegrationMode) }
HostIntegrationSection(mode: $draft.shellIntegrationMode)
}
)
}
@@ -1,49 +0,0 @@
import SwiftUI
import TermindApplication
import TermindDomain
import UIKit
/// Content-sized segments keep the longer follow-global label readable on phones.
struct MobileHostIntegrationPicker: UIViewRepresentable {
private static let modes: [HostShellIntegrationMode] = [.enabled, .followGlobalSettings, .disabled]
@Binding var mode: HostShellIntegrationMode
@Environment(\.locale) private var locale
func makeUIView(context: Context) -> UISegmentedControl {
let control = UISegmentedControl(items: ["", "", ""])
control.apportionsSegmentWidthsByContent = true
control.addTarget(context.coordinator, action: #selector(Coordinator.select(_:)), for: .valueChanged)
control.setContentHuggingPriority(.defaultLow, for: .horizontal)
return control
}
func updateUIView(_ control: UISegmentedControl, context: Context) {
context.coordinator.mode = $mode
let labels = [
AppLocalization.string("Enable", locale: locale),
AppLocalization.string("Follow Global Settings", locale: locale),
AppLocalization.string("Disable", locale: locale),
]
for (index, label) in labels.enumerated() { control.setTitle(label, forSegmentAt: index) }
control.accessibilityLabel = AppLocalization.string("Shell Integration", locale: locale)
control.selectedSegmentIndex = Self.modes.firstIndex(of: mode) ?? 1
control.isEnabled = context.environment.isEnabled
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UISegmentedControl, context: Context) -> CGSize? {
CGSize(width: proposal.width ?? uiView.intrinsicContentSize.width, height: uiView.intrinsicContentSize.height)
}
func makeCoordinator() -> Coordinator { Coordinator(mode: $mode) }
final class Coordinator: NSObject {
var mode: Binding<HostShellIntegrationMode>
init(mode: Binding<HostShellIntegrationMode>) { self.mode = mode }
@objc func select(_ control: UISegmentedControl) {
guard MobileHostIntegrationPicker.modes.indices.contains(control.selectedSegmentIndex) else { return }
mode.wrappedValue = MobileHostIntegrationPicker.modes[control.selectedSegmentIndex]
}
}
}
@@ -15,9 +15,7 @@ struct MobileHostIntegrationRenderingTests {
let controller = UIHostingController(
rootView: Form {
Section("Group") { Text("Production") }
HostIntegrationSection {
MobileHostIntegrationPicker(mode: Binding(get: { draft.mode }, set: { draft.mode = $0 }))
}
HostIntegrationSection(mode: Binding(get: { draft.mode }, set: { draft.mode = $0 }))
}
.environment(\.locale, Locale(identifier: language)))
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
@@ -34,22 +32,32 @@ struct MobileHostIntegrationRenderingTests {
}
try await Task.sleep(for: .milliseconds(500))
controller.view.layoutIfNeeded()
let picker = try #require(descendants(controller.view).compactMap { $0 as? UISegmentedControl }.first)
#expect(picker.numberOfSegments == 3)
#expect(picker.apportionsSegmentWidthsByContent)
#expect(picker.selectedSegmentIndex == 1)
#expect(
picker.titleForSegment(at: 1)
== AppLocalization.string("Follow Global Settings", locale: Locale(identifier: language)))
picker.selectedSegmentIndex = 0
picker.sendActions(for: .valueChanged)
#expect(descendants(controller.view).compactMap { $0 as? UISegmentedControl }.isEmpty)
let picker = try #require(
descendants(controller.view).compactMap { $0 as? UIButton }.first { $0.showsMenuAsPrimaryAction })
let labels = ["Default", "Enable", "Disable"].map {
AppLocalization.string($0, locale: Locale(identifier: language))
}
let actions = try await menuActions(in: picker)
#expect(actions.map(\.title) == labels)
#expect(actions.map(\.state) == [.on, .off, .off])
picker.sendAction(actions[1])
#expect(draft.mode == .enabled)
picker.selectedSegmentIndex = 2
picker.sendActions(for: .valueChanged)
controller.view.frame.size.width = 600
controller.view.layoutIfNeeded()
let enabledActions = try await menuActions(in: picker)
#expect(enabledActions.map(\.title) == labels)
#expect(enabledActions.map(\.state) == [.off, .on, .off])
picker.sendAction(enabledActions[2])
#expect(draft.mode == .disabled)
picker.selectedSegmentIndex = 1
picker.sendActions(for: .valueChanged)
controller.view.frame.size.width = 375
controller.view.layoutIfNeeded()
let disabledActions = try await menuActions(in: picker)
#expect(disabledActions.map(\.title) == labels)
#expect(disabledActions.map(\.state) == [.off, .off, .on])
picker.sendAction(disabledActions[0])
#expect(draft.mode == .followGlobalSettings)
try await Task.sleep(for: .milliseconds(300))
let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in
#expect(window.drawHierarchy(in: window.bounds, afterScreenUpdates: true))
}
@@ -63,6 +71,29 @@ struct MobileHostIntegrationRenderingTests {
private func descendants(_ view: UIView) -> [UIView] {
[view] + view.subviews.flatMap(descendants)
}
private func menuActions(in button: UIButton) async throws -> [UIAction] {
button.performPrimaryAction()
try await Task.sleep(for: .milliseconds(500))
let interaction = try #require(button.contextMenuInteraction)
var actions: [UIAction] = []
interaction.updateVisibleMenu { menu in
actions = self.menuActions(menu)
return menu
}
interaction.dismissMenu()
try await Task.sleep(for: .milliseconds(300))
try #require(actions.count == 3)
return actions
}
private func menuActions(_ menu: UIMenu) -> [UIAction] {
menu.children.flatMap { element -> [UIAction] in
if let action = element as? UIAction { return [action] }
if let submenu = element as? UIMenu { return menuActions(submenu) }
return []
}
}
}
@Observable @MainActor private final class HostIntegrationDraft {
+2 -4
View File
@@ -274,13 +274,11 @@ nonisolated enum Metrics {
enum VaultContent {
static let min: CGFloat = 280
static let ideal: CGFloat = 680
static let splitMinWidth: CGFloat = min + 1 + Metrics.VaultDetail.min
static let splitMinWidth: CGFloat = min + 1 + Metrics.VaultDetail.width
}
enum VaultDetail {
static let min: CGFloat = 360
static let ideal: CGFloat = 400
static let max: CGFloat = 720
static let width: CGFloat = 320
}
enum Window {
+1 -1
View File
@@ -167,7 +167,7 @@ struct HostEditorView: View {
}
}
HostIntegrationSection { HostShellIntegrationPicker(mode: $draft.shellIntegrationMode) }
HostIntegrationSection(mode: $draft.shellIntegrationMode)
if !draft.host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Section("SSH Command") {
+1 -2
View File
@@ -80,8 +80,7 @@ struct VaultWorkspaceLayout: Layout {
let progress = min(1, max(0, expansion))
// Keep the editor at its open width even while it leaves. Only its
// position and the list's reserved space animate, never the form's width.
let detailWidth =
split ? min(Metrics.VaultDetail.ideal, bounds.width - Metrics.VaultContent.min - 1) : bounds.width
let detailWidth = split ? Metrics.VaultDetail.width : bounds.width
let listWidth = split ? bounds.width - (detailWidth + 1) * progress : bounds.width
let listIsVisible = !showsDetail || split
let listSize = listIsVisible ? CGSize(width: listWidth, height: bounds.height) : cache.listSize ?? bounds.size
+4 -1
View File
@@ -141,7 +141,10 @@ struct VaultDetailScaffold<Content: View, Footer: View>: View {
Label(title, systemImage: symbol)
.font(.headline)
Spacer()
.lineLimit(1)
.truncationMode(.tail)
.help(title)
Spacer(minLength: 0)
}
.padding(.horizontal, Metrics.Space.l)
.padding(.vertical, Metrics.Space.m)
+14 -13
View File
@@ -8,22 +8,22 @@ import Testing
@Suite(.serialized) @MainActor
struct HostIntegrationRenderingTests {
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func hostEditorShowsIntegrationBelowGroupingAtNarrowWidth(language: String) async throws {
@Test(arguments: ["en", "zh-Hans", "zh-Hant"], [320.0, 360.0, 480.0])
func hostEditorShowsIntegrationBelowGroupingAtNarrowWidth(language: String, width: CGFloat) async throws {
let host = TermindDomain.Host(name: "Production", host: "server.invalid")
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
let navigation = WindowModel()
let controller = NSHostingController(
rootView: HostEditorView(hostID: host.id).environment(app).environment(navigation)
.environment(\.locale, Locale(identifier: language))
.frame(width: 360, height: 1400))
.frame(width: width, height: 1400))
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 360, height: 1400),
contentRect: NSRect(x: 0, y: 0, width: width, height: 1400),
styleMask: [.borderless], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
controller.sizingOptions = []
window.contentViewController = controller
controller.view.frame = NSRect(x: 0, y: 0, width: 360, height: 1400)
controller.view.frame = NSRect(x: 0, y: 0, width: width, height: 1400)
window.orderFront(nil)
defer {
app.shutdownForTermination()
@@ -33,18 +33,19 @@ struct HostIntegrationRenderingTests {
}
try await Task.sleep(for: .milliseconds(500))
controller.view.layoutSubtreeIfNeeded()
let control = try #require(descendants(controller.view).compactMap { $0 as? NSSegmentedControl }.first)
#expect(control.segmentCount == 3)
#expect(control.selectedSegment == 1)
let labels = (0..<3).map { control.label(forSegment: $0) ?? "" }
#expect(labels.allSatisfy { !$0.isEmpty })
#expect(labels[1] == AppLocalization.string("Follow Global Settings", locale: Locale(identifier: language)))
let defaultLabel = AppLocalization.string("Default", locale: Locale(identifier: language))
#expect(descendants(controller.view).compactMap { $0 as? NSSegmentedControl }.isEmpty)
let control = try #require(
descendants(controller.view).compactMap { $0 as? NSPopUpButton }.first { $0.title == defaultLabel })
let frame = control.convert(control.bounds, to: controller.view)
#expect(frame.minX >= 16 && frame.maxX <= width - 16)
let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds))
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
let data = try #require(bitmap.representation(using: .png, properties: [:]))
Attachment.record(data, named: "host-integration-mac-\(language).png")
Attachment.record(data, named: "host-integration-mac-\(language)-\(Int(width)).png")
try data.write(
to: FileManager.default.temporaryDirectory.appending(path: "host-integration-mac-\(language).png"))
to: FileManager.default.temporaryDirectory.appending(
path: "host-integration-mac-\(language)-\(Int(width)).png"))
}
private func descendants(_ view: NSView) -> [NSView] {
+6 -5
View File
@@ -123,7 +123,7 @@ struct PortForwardingRenderingTests {
let prompt = try #require(context.authenticationPrompt)
navigation.openVaultWork(.forwardingEditor(rules[0].id), on: .portForwarding)
var passwordField: NSSecureTextField?
for width in [960.0, 360.0, 640.0, 641.0, 681.0, 960.0] {
for width in [960.0, 360.0, 600.0, 601.0, 640.0, 641.0, 681.0, 960.0] {
window.setContentSize(NSSize(width: width, height: 440))
controller.view.frame = NSRect(x: 0, y: 0, width: width, height: 440)
try await Task.sleep(for: .milliseconds(300))
@@ -172,7 +172,8 @@ struct PortForwardingRenderingTests {
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func embeddedJourneyDecisionsAndLogsFitShortNarrowColumns(_ language: String) async throws {
let host = Host(name: "Production Database Gateway with a Long Name", host: "gateway.example.invalid")
let host = Host(
name: "AWS US-West-1 Production Database Gateway with a Long Name", host: "gateway.example.invalid")
let jump = Host(name: "Jump Host", host: "jump.example.invalid")
let app = AppModel(hosts: HostLibraryModel(hosts: [host, jump]))
defer { app.shutdownForTermination() }
@@ -208,7 +209,7 @@ struct PortForwardingRenderingTests {
journey.showsLogs = true
}
let controller = NSHostingController(
rootView: VaultDetailScaffold(title: "Port Forwarding", symbol: "arrow.left.arrow.right", close: {}) {
rootView: VaultDetailScaffold(title: "\(host.name)", symbol: "arrow.left.arrow.right", close: {}) {
ConnectionJourneyView(
journey: journey, hostKey: hostKey,
submitAuthentication: { _, _, _, _, _, _ in actions += 1 },
@@ -224,7 +225,7 @@ struct PortForwardingRenderingTests {
.environment(app)
.environment(\.locale, Locale(identifier: language)))
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 440),
contentRect: NSRect(x: 0, y: 0, width: 320, height: 440),
styleMask: [.titled, .resizable], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
controller.sizingOptions = []
@@ -238,7 +239,7 @@ struct PortForwardingRenderingTests {
window.close()
}
var fields: [NSTextField]?
for width in [400.0, 360.0, 400.0] {
for width in [320.0, 360.0, 320.0] {
window.setContentSize(NSSize(width: width, height: 440))
controller.view.frame = NSRect(x: 0, y: 0, width: width, height: 440)
try await Task.sleep(for: .milliseconds(100))
+74 -10
View File
@@ -1,6 +1,7 @@
import AppKit
import SwiftUI
import TermindApplication
import TermindDomain
import Testing
@testable import Termind
@@ -8,6 +9,65 @@ import Testing
@MainActor
@Suite(.serialized)
struct ResponsivePageLayoutTests {
@Test(arguments: ["en", "zh-Hans", "zh-Hant"])
func vaultEditorsKeepFieldsInsideNarrowColumns(_ language: String) async throws {
let host = TermindDomain.Host(name: "Production Database Gateway with a Long Name", host: "server.invalid")
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
let navigation = WindowModel()
defer { app.shutdownForTermination() }
let editors: [(String, VaultWorkRoute)] = [
("host", .hostEditor(host.id)),
("group", .hostGroupEditor(groupID: nil, parentID: nil)),
("identity", .identityEditor(nil)),
("known-host", .knownHostEditor(nil)),
("forwarding", .forwardingEditor(nil)),
("snippet", .snippetEditor(snippetID: nil, packageID: nil)),
("package", .snippetPackageEditor(packageID: nil, parentID: nil)),
("generate-key", .generateKey),
]
for (name, route) in editors {
let controller = NSHostingController(
rootView: VaultWorkDetailView(route: route).environment(app).environment(navigation)
.environment(\.locale, Locale(identifier: language)))
let window = NSWindow(
contentRect: CGRect(x: 0, y: 0, width: 320, height: 640),
styleMask: .borderless, backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
controller.sizingOptions = []
window.contentViewController = controller
window.setContentSize(CGSize(width: 320, height: 640))
controller.view.frame = CGRect(x: 0, y: 0, width: 320, height: 640)
window.orderFront(nil)
defer {
window.orderOut(nil)
window.contentViewController = nil
window.close()
}
await layout(controller.view)
#expect(abs(controller.view.bounds.width - 320) <= 1)
let scroll = try #require(descendant(in: controller.view, ofType: NSScrollView.self))
let document = try #require(scroll.documentView)
#expect(document.frame.width <= scroll.contentView.bounds.width + 1, "\(name) overflows horizontally")
#expect(scroll.contentInsets.left >= 0 && scroll.contentInsets.right >= 0)
#expect(!scroll.hasHorizontalScroller)
let fields = descendants(in: controller.view).compactMap { $0 as? NSTextField }
.filter { $0.isEditable && !$0.isHiddenOrHasHiddenAncestor }
#expect(!fields.isEmpty)
for field in fields {
let frame = field.convert(field.bounds, to: controller.view)
#expect(frame.minX >= 0 && frame.maxX <= 321, "\(name): \(frame)")
}
let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds))
controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap)
let data = try #require(bitmap.representation(using: .png, properties: [:]))
let filename = "macos-vault-\(name)-\(language)-320.png"
Attachment.record(data, named: filename)
let directory = FileManager.default.temporaryDirectory.appending(path: "termind-vault-ui")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try data.write(to: directory.appending(path: filename))
}
}
@Test
func metricsPagesAndResizeKeepTheProcessTableAndBothScrollPositions() async throws {
let configuration = ScreenshotDemoConfiguration(scene: .metricsDetail, language: .english)
@@ -97,10 +157,10 @@ struct ResponsivePageLayoutTests {
let listView = try #require(list.view)
let editorView = try #require(editor.view)
editorView.stringValue = "Unsaved host name"
#expect(abs(editorView.bounds.width - 400) <= 1)
#expect(abs(editorView.bounds.width - 320) <= 1)
let listSize = listView.bounds.size
for width in [360.0, 640.0] {
for width in [360.0, 600.0] {
host.frame.size.width = width
await layout(host)
#expect(editor.view === editorView)
@@ -109,17 +169,17 @@ struct ResponsivePageLayoutTests {
#expect(listView.bounds.size == listSize)
}
host.frame.size.width = 641
host.frame.size.width = 601
await layout(host)
#expect(editorView.stringValue == "Unsaved host name")
#expect(abs(editorView.bounds.width - 360) <= 1)
#expect(abs(editorView.bounds.width - 320) <= 1)
#expect(abs(listView.bounds.width - 280) <= 1)
for width in [681.0, 900.0] {
for width in [640.0, 641.0, 681.0, 900.0] {
host.frame.size.width = width
await layout(host)
#expect(abs(editorView.bounds.width - 400) <= 1)
#expect(abs(listView.bounds.width - (width - 401)) <= 1)
#expect(abs(editorView.bounds.width - 320) <= 1)
#expect(abs(listView.bounds.width - (width - 321)) <= 1)
#expect(editorView.stringValue == "Unsaved host name")
}
@@ -133,7 +193,7 @@ struct ResponsivePageLayoutTests {
#expect(editor.creations == 1)
}
@Test(arguments: [360.0, 900.0])
@Test(arguments: [360.0, 600.0, 601.0, 900.0])
func vaultOpeningAndClosingSlideWithoutSqueezingTheEditor(_ width: CGFloat) async throws {
let list = Probe()
let editor = Probe()
@@ -154,8 +214,8 @@ struct ResponsivePageLayoutTests {
let frame = editorView.convert(editorView.bounds, to: host)
if progress == 0.5 {
#expect(frame.minX > width - editorWidth && frame.minX < width)
if width == 900 {
#expect(listView.bounds.width > 499 && listView.bounds.width < 900)
if width >= 601 {
#expect(abs(listView.bounds.width - (width - 321 * progress)) <= 1)
}
} else if progress == 1 {
#expect(abs(frame.maxX - width) <= 1)
@@ -181,6 +241,10 @@ struct ResponsivePageLayoutTests {
return view.subviews.lazy.compactMap { descendant(in: $0, ofType: type) }.first
}
private func descendants(in view: NSView) -> [NSView] {
[view] + view.subviews.flatMap { descendants(in: $0) }
}
private final class Probe {
var view: NSTextField?
var creations = 0