Files
Termind/TermindTests/WindowStateTests.swift
T

425 lines
16 KiB
Swift

import Foundation
import Testing
@testable import Termind
@Suite
@MainActor
struct WindowStateTests {
@Test
func newWindowStartsOnWelcome() {
let window = WindowModel()
#expect(window.selectedTopLevelTab == .vault)
#expect(window.vaultDestination == .welcome)
}
@Test
func pinnedVaultAndAssistantPrecedeDynamicWorkspaces() {
let host = Host(name: "Server", host: "server.example")
let workspace = Workspace.sshSession(host: host)
let window = WindowModel(tabs: [workspace])
window.select(index: 0)
#expect(window.selectedTopLevelTab == .vault)
window.select(index: 1)
#expect(window.selectedTopLevelTab == .assistant)
window.select(index: 2)
#expect(window.selectedTopLevelTab == .workspace(workspace.id))
window.selectVault()
window.closeSelected()
#expect(window.tabs.map(\.id) == [workspace.id])
}
@Test
func assistantFeatureOwnsConversationLifecycleOutsideWorkspaces() {
let window = WindowModel()
let first = window.openAssistant()
let second = window.openAssistant()
#expect(window.selectedTopLevelTab == .assistant)
#expect(window.tabs.isEmpty)
#expect(window.assistantLibrary.conversations.map(\.id) == [second.id, first.id])
window.assistantLibrary.delete(second.id)
#expect(window.assistantLibrary.selectedConversationID == first.id)
}
@Test
func vaultRoutesNeverBecomeWorkspaceTabs() {
let host = Host(name: "Server", host: "server.example")
let workspace = Workspace.sshSession(host: host)
let window = WindowModel(tabs: [workspace])
window.openPage(.hosts)
#expect(window.selectedTopLevelTab == .vault)
#expect(window.vaultDestination == .hosts)
#expect(window.tabs.map(\.id) == [workspace.id])
window.select(workspace.id)
#expect(window.selectedTopLevelTab == .workspace(workspace.id))
}
@Test
func draggingSelectedWorkspaceDisplaysNeighbourAsMergeTarget() {
let first = Workspace.sshSession(host: Host(name: "First", host: "first.example"))
let second = Workspace.sshSession(host: Host(name: "Second", host: "second.example"))
let window = WindowModel(tabs: [first, second])
window.select(first.id)
window.dragging = .workspace(first.id)
#expect(window.displayedTab?.id == second.id)
#expect(window.selectedTab?.id == first.id)
}
@Test
func hostEditorIsVaultNavigationAndCloseTabCannotCloseHiddenWork() {
let host = Host(name: "Server", host: "server.example")
let workspace = Workspace.sshSession(host: host)
let window = WindowModel(tabs: [workspace])
window.openHostEditor(host: host)
#expect(window.selectedTopLevelTab == .vault)
#expect(window.vaultDestination == .hosts)
#expect(window.vaultWorkRoute == .hostEditor(host.id))
window.closeSelected()
#expect(window.tabs.map(\.id) == [workspace.id])
}
@Test
func changingVaultPageClosesRightColumnWork() {
let window = WindowModel(vaultDestination: .hosts)
window.openVaultWork(
.snippetEditor(snippetID: nil, packageID: nil), on: .snippets
)
#expect(window.vaultDestination == .snippets)
#expect(window.vaultWorkRoute == .snippetEditor(snippetID: nil, packageID: nil))
window.openPage(.keychain)
#expect(window.vaultDestination == .keychain)
#expect(window.vaultWorkRoute == nil)
}
@Test
func windowsKeepPresentationAndContextPanelStateIsolated() {
let first = WindowModel(vaultDestination: .hosts)
let second = WindowModel(vaultDestination: .hosts)
let host = UUID()
first.hostChooser = .connect
first.selectDetail(.host(host), on: .hosts)
#expect(first.hostChooser == .connect)
#expect(second.hostChooser == nil)
#expect(second.detailSelection(for: .hosts).isEmpty)
}
@Test
func windowSheetRouteIsMutuallyExclusive() {
let state = WindowPresentationState()
state.hostChooser = .connect
#expect(state.hostChooser == .connect)
state.hostChooser = .batchStartup
#expect(state.hostChooser == .batchStartup)
state.hostChooser = nil
#expect(state.sheet == nil)
}
@Test
func contextPanelOverrideExpiresWhenFocusLeavesItsPane() {
let firstHost = Host(name: "First", host: "first.example")
let secondHost = Host(name: "Second", host: "second.example")
let first = Workspace.sshSession(host: firstHost)
let second = Workspace.sshSession(host: secondHost)
let window = WindowModel(tabs: [first, second])
window.selectedTabID = first.id
window.sidebar.show(.snippet(UUID()), scopedTo: first.focusedPane)
#expect(window.activeDetailSelection.first?.kind == ContextPanelDetailID.Kind.snippet)
window.selectedTabID = second.id
window.sidebar.focusedPaneDidChange(to: second.focusedPane)
#expect(window.activeDetailSelection == [.host(secondHost.id)])
}
@Test
func featureRoutesReplaceRatherThanStack() {
let window = WindowModel(vaultDestination: .hosts)
let groupID = UUID()
window.openVaultWork(
.hostGroupEditor(groupID: nil, parentID: nil), on: .hosts
)
window.openVaultWork(
.hostGroupEditor(groupID: groupID, parentID: nil), on: .hosts
)
#expect(
window.vaultWorkRoute
== .hostGroupEditor(groupID: groupID, parentID: nil)
)
let snippets = SnippetsPageState()
let batch = SnippetBatch(snippets: [])
snippets.confirmation = .executeDestructive(batch)
snippets.confirmation = .deleteSnippets(batch)
guard case .deleteSnippets = snippets.confirmation else {
Issue.record("Expected one active snippet confirmation route")
return
}
}
@Test
func sessionsMixSSHAndSFTPInTabAndPaneOrder() {
let firstHost = Host(name: "First", host: "first.example")
let secondHost = Host(name: "Second", host: "second.example")
let mixed = Workspace.sshSession(host: firstHost)
let sshPane = mixed.focusedPane
mixed.splitFocusedPane(axis: .horizontal, with: .sftpSession(host: firstHost))
let sftpPane = mixed.focusedPane
let secondSSH = Workspace.sshSession(host: secondHost)
let window = WindowModel(tabs: [mixed, secondSSH])
#expect(window.sessionPanes.map(\.pane.id) == [sshPane.id, sftpPane.id, secondSSH.focusedPane.id])
#expect(window.sshSessionPanes.map(\.pane.id) == [sshPane.id, secondSSH.focusedPane.id])
#expect(window.sftpSessionPanes.map(\.pane.id) == [sftpPane.id])
#expect(window.sessionPanes.map(\.pane.symbol) == ["apple.terminal", "folder", "apple.terminal"])
}
@Test
func sftpTitlesUseHostNameWithoutPrefixAndFallBackToSFTP() {
let host = Host(name: "Uploads", host: "uploads.example")
let store = HostStore(hosts: [host])
let pane = Pane.sftpSession(host: host)
let missing = Pane(.sftpSession(SFTPSessionModel(hostID: UUID(), initialPath: "/")))
#expect(pane.title(in: store) == "Uploads")
#expect(pane.sidebarTitle(in: store) == "Uploads")
#expect(missing.title(in: store) == "SFTP")
#expect(missing.sidebarTitle(in: store) == "SFTP")
}
@Test
func selectingAndClosingUnifiedSessionPanesKeepsWorkspaceStateConsistent() {
let host = Host(name: "Host", host: "host.example")
let workspace = Workspace.sshSession(host: host)
let sshPane = workspace.focusedPane
workspace.splitFocusedPane(axis: .horizontal, with: .sftpSession(host: host))
let sftpPane = workspace.focusedPane
let window = WindowModel(tabs: [workspace])
workspace.focus(sshPane.id)
window.select(workspace.id)
workspace.focus(sftpPane.id)
window.closePane(sftpPane.id)
#expect(window.sessionPanes.map(\.pane.id) == [sshPane.id])
#expect(workspace.focusedPaneID == sshPane.id)
window.closePane(sshPane.id)
#expect(window.sessionPanes.isEmpty)
#expect(window.tabs.isEmpty)
}
@Test
func appRoutesHostsToSSHAndSFTPSessionsForTabsAndWorkspaces() {
let first = Host(name: "First", host: "first.example")
let second = Host(name: "Second", host: "second.example")
let store = HostStore(hosts: [first, second])
let app = AppModel(hosts: store)
let window = WindowModel()
let sshTab = app.open(first, as: .sshSession, in: window)
let sftpTab = app.open(second, as: .sftpSession, in: window)
#expect(sshTab.focusedPane.kind == .sshSession)
#expect(sftpTab.focusedPane.kind == .sftpSession)
app.open([first, second], as: .sshSession, layout: .separateTabs, in: window)
#expect(window.selectedTabID == window.tabs[2].id)
#expect(window.tabs.suffix(2).map(\.focusedPane.kind) == [.sshSession, .sshSession])
app.open([first, second], as: .sftpSession, layout: .oneWorkspace, in: window)
#expect(window.selectedTab?.panes.map(\.kind) == [.sftpSession, .sftpSession])
}
@Test
func reopeningAClosedTerminalCreatesAFreshConnectingSession() throws {
let host = Host(name: "Server", host: "server.example")
let workspace = Workspace.sshSession(host: host)
let pane = workspace.focusedPane
pane.customTitle = "Logs"
let closedSession = try #require(pane.sshSession)
let window = WindowModel(tabs: [workspace])
window.close(workspace.id)
#expect(closedSession.status == .disconnected)
#expect(window.canReopen)
window.reopenLastClosed()
let reopenedPane = try #require(window.selectedTab?.focusedPane)
let reopenedSession = try #require(reopenedPane.sshSession)
#expect(reopenedSession !== closedSession)
#expect(reopenedSession.hostID == host.id)
#expect(reopenedSession.status == .connecting)
#expect(reopenedPane.customTitle == "Logs")
#expect(window.selectedTab === workspace)
#expect(!window.canReopen)
}
@Test
func terminalPathOpensSFTPInANewTabOrTheCurrentWorkspace() {
var host = Host(name: "Files", host: "files.example")
host.sftpSessionInitialPath = "/configured"
let sshWorkspace = Workspace.sshSession(host: host)
let window = WindowModel(tabs: [sshWorkspace])
let newTab = window.openSFTPSession(
host: host, initialPath: "/var/log/", placement: .newTab
)
#expect(window.tabs.count == 2)
#expect(window.selectedTab === newTab)
#expect(newTab.focusedPane.sftpSession?.path == "/var/log")
#expect(newTab.focusedPane.sftpSession?.initialPath == "/var/log")
#expect(host.sftpSessionInitialPath == "/configured")
window.select(sshWorkspace.id)
let result = window.openSFTPSession(
host: host, initialPath: "/srv/app", placement: .currentWorkspace
)
#expect(result === sshWorkspace)
#expect(window.tabs.count == 2)
#expect(sshWorkspace.panes.map(\.kind) == [.sshSession, .sftpSession])
#expect(sshWorkspace.focusedPane.sftpSession?.path == "/srv/app")
}
@Test
func sftpPathOpensSSHInANewTabOrTheCurrentWorkspace() throws {
let host = Host(name: "Shell", host: "shell.example")
let sftpWorkspace = Workspace.sftpSession(host: host, initialPath: "/configured")
let window = WindowModel(tabs: [sftpWorkspace])
let newTab = window.openSSHSession(
host: host, initialPath: "/var/log", placement: .newTab
)
#expect(window.tabs.count == 2)
#expect(window.selectedTab === newTab)
#expect(newTab.focusedPane.sshSession?.hostID == host.id)
window.select(sftpWorkspace.id)
let result = window.openSSHSession(
host: host, initialPath: "/srv/app", placement: .currentWorkspace
)
#expect(result === sftpWorkspace)
#expect(window.tabs.count == 2)
#expect(sftpWorkspace.panes.map(\.kind) == [.sftpSession, .sshSession])
#expect(sftpWorkspace.focusedPane.sshSession?.hostID == host.id)
}
@Test
func sshDirectoryCommandQuotesRemotePaths() {
#expect(
WindowModel.changeDirectoryCommand(for: "/srv/it's live")
== "cd '/srv/it'\"'\"'s live'"
)
#expect(WindowModel.changeDirectoryCommand(for: "") == nil)
}
@Test
func sftpWithoutAnOverrideKeepsTheHostsConfiguredInitialPath() {
var host = Host(name: "Files", host: "files.example")
host.sftpSessionInitialPath = "/configured"
let window = WindowModel()
let workspace = window.openSFTPSession(host: host)
#expect(workspace.focusedPane.sftpSession?.path == "/configured")
#expect(workspace.focusedPane.sftpSession?.initialPath == "/configured")
}
@Test
func duplicateDestinationsPreservePaneAndWorkspaceScope() {
let host = Host(name: "Host", host: "host.example")
let original = Workspace.sshSession(host: host)
let window = WindowModel(tabs: [original])
window.duplicateSelected(axis: .horizontal)
#expect(window.tabs.count == 1)
#expect(original.panes.count == 2)
let focusedPane = original.focusedPane
window.duplicatePaneInNewTab()
#expect(window.tabs.count == 2)
#expect(window.selectedTab?.panes.count == 1)
#expect(window.selectedTab?.focusedPane.kind == focusedPane.kind)
window.select(original.id)
window.duplicateSelectedInNewTab()
#expect(window.tabs.count == 3)
#expect(window.selectedTab?.panes.count == 2)
#expect(window.selectedTab?.panes.map(\.kind) == original.panes.map(\.kind))
}
@Test
func normalRemoteExitClosesOnlyItsSplitPane() {
let firstHost = Host(name: "First", host: "first.example")
let secondHost = Host(name: "Second", host: "second.example")
let workspace = Workspace.sshSession(host: firstHost)
let firstPane = workspace.focusedPane
let firstSSHSession = firstPane.sshSession!
workspace.splitFocusedPane(axis: .horizontal, with: .sshSession(host: secondHost))
let secondPane = workspace.focusedPane
workspace.focus(firstPane.id)
workspace.broadcastsInput = true
let window = WindowModel(tabs: [workspace])
firstSSHSession.handleRemoteExit(.success(0))
#expect(window.tabs.count == 1)
#expect(workspace.panes.map(\.id) == [secondPane.id])
#expect(workspace.layout.pane(firstPane.id) == nil)
#expect(workspace.focusedPaneID == secondPane.id)
#expect(!workspace.broadcastsInput)
#expect(firstSSHSession.inputRouter == nil)
}
@Test
func normalRemoteExitClosesLastPaneAndSelectsNeighboringTab() {
let host = Host(name: "Host", host: "host.example")
let sshSessionTab = Workspace.sshSession(host: host)
let sshSession = sshSessionTab.focusedPane.sshSession!
let neighbor = Workspace.sftpSession(host: host)
let window = WindowModel(tabs: [sshSessionTab, neighbor])
window.select(sshSessionTab.id)
sshSession.handleRemoteExit(.success(0))
#expect(window.tabs.map(\.id) == [neighbor.id])
#expect(window.selectedTabID == neighbor.id)
}
@Test
func transportFailureKeepsPaneOpenForReconnect() {
let host = Host(name: "Host", host: "host.example")
let workspace = Workspace.sshSession(host: host)
let sshSession = workspace.focusedPane.sshSession!
let window = WindowModel(tabs: [workspace])
sshSession.handleRemoteExit(.failure(.disconnected))
#expect(window.tabs.map(\.id) == [workspace.id])
#expect(workspace.focusedPane.sshSession === sshSession)
guard case .failed = sshSession.status else {
Issue.record("Expected transport failure to remain visible")
return
}
}
}