266 lines
9.5 KiB
Swift
266 lines
9.5 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import TermindPresentation
|
|
import TermindRemoteInfrastructure
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
/// The floating capsule answers two questions with one rule: does it appear at
|
|
/// all, and what does it shrink to. Splitting them is how you get a capsule on
|
|
/// screen with nothing to collapse into, or a collapsed dot for a session that
|
|
/// is not there.
|
|
@Suite
|
|
struct StatusBarPresentationTests {
|
|
@Test
|
|
func aSessionCollapsesToItsConnection() {
|
|
let glyph = StatusBarPresentation.collapsedGlyph(
|
|
hasSessionContext: true, status: .connected, hasTransfers: false
|
|
)
|
|
|
|
#expect(glyph == .connection(.connected))
|
|
}
|
|
|
|
/// The dot's colour is the whole reason this is the collapsed form, so it
|
|
/// has to carry whatever the connection is actually doing.
|
|
@Test
|
|
func theCollapsedDotCarriesTheLiveStatus() {
|
|
for status: ConnectionStatus in [
|
|
.disconnected, .connecting, .connected, .reconnecting, .failed("refused"),
|
|
] {
|
|
#expect(
|
|
StatusBarPresentation.collapsedGlyph(
|
|
hasSessionContext: true, status: status, hasTransfers: false
|
|
) == .connection(status)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A transfer with no pane in front of it is the one case where the capsule
|
|
/// is about something other than a session.
|
|
@Test
|
|
func transfersWithoutASessionCollapseToTheTransferGlyph() {
|
|
let glyph = StatusBarPresentation.collapsedGlyph(
|
|
hasSessionContext: false, status: .disconnected, hasTransfers: true
|
|
)
|
|
|
|
#expect(glyph == .transfers)
|
|
}
|
|
|
|
/// A transfer is a thing that is happening; the connection is the thing you
|
|
/// are looking at.
|
|
@Test
|
|
func theConnectionWinsWhenBothAreThere() {
|
|
let glyph = StatusBarPresentation.collapsedGlyph(
|
|
hasSessionContext: true, status: .connected, hasTransfers: true
|
|
)
|
|
|
|
#expect(glyph == .connection(.connected))
|
|
}
|
|
|
|
/// Nothing to say means no capsule — not an empty one floating over the pane.
|
|
@Test
|
|
func nothingToSayShowsNoCapsule() {
|
|
let glyph = StatusBarPresentation.collapsedGlyph(
|
|
hasSessionContext: false, status: .disconnected, hasTransfers: false
|
|
)
|
|
|
|
#expect(glyph == nil)
|
|
}
|
|
|
|
/// Collapsing changes the capsule's width, never its height, so the command
|
|
/// generator and the upload notices can reserve one constant. If the height
|
|
/// ever moves, this is what says the clearance moved with it.
|
|
@Test
|
|
func bottomOverlaysClearTheCapsuleInBothStates() {
|
|
#expect(
|
|
Metrics.StatusBar.overlayClearance
|
|
== Metrics.StatusBar.height + Metrics.StatusBar.floatingInset * 2
|
|
)
|
|
#expect(Metrics.StatusBar.overlayClearance > Metrics.StatusBar.height)
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func transferGlyphKeepsTheSameSizeAcrossStates() throws {
|
|
let running = transfer(
|
|
direction: .upload,
|
|
state: .running,
|
|
byteCount: 100,
|
|
transferredBytes: 50
|
|
)
|
|
let indeterminate = transfer(direction: .download, state: .running)
|
|
let failed = transfer(direction: .download, state: .failed("Offline"))
|
|
let completed = transfer(direction: .upload, state: .completed)
|
|
|
|
for transfers in [[running], [indeterminate], [failed], [completed]] {
|
|
let coordinator = SFTPTransferCoordinator(
|
|
store: HostLibraryModel(sftpTransfers: transfers)
|
|
)
|
|
let renderer = ImageRenderer(
|
|
content: TransferGlyph(summary: coordinator.summary, transfers: transfers)
|
|
)
|
|
let image = try #require(renderer.nsImage)
|
|
|
|
#expect(
|
|
image.size
|
|
== CGSize(width: TransferGlyph.size, height: TransferGlyph.size)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func runningUploadsUseTheUploadSymbol() {
|
|
let transfers = [
|
|
transfer(direction: .upload, state: .running),
|
|
transfer(direction: .upload, state: .running),
|
|
]
|
|
|
|
#expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.up")
|
|
}
|
|
|
|
@Test
|
|
func idleTransfersUseTheBidirectionalSymbolWithoutACircle() {
|
|
#expect(StatusBarTransferPresentation.idleDirectionSymbol == "arrow.up.arrow.down")
|
|
}
|
|
|
|
@Test
|
|
func runningDownloadsUseTheDownloadSymbol() {
|
|
let transfers = [
|
|
transfer(direction: .download, state: .running),
|
|
transfer(direction: .download, state: .running),
|
|
]
|
|
|
|
#expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.down")
|
|
}
|
|
|
|
@Test
|
|
func copiesAndMixedDirectionsUseTheBidirectionalSymbol() {
|
|
#expect(
|
|
StatusBarTransferPresentation.runningDirectionSymbol(
|
|
for: [transfer(direction: .copy, state: .running)]
|
|
) == "arrow.up.arrow.down"
|
|
)
|
|
#expect(
|
|
StatusBarTransferPresentation.runningDirectionSymbol(
|
|
for: [
|
|
transfer(direction: .upload, state: .running),
|
|
transfer(direction: .download, state: .running),
|
|
]
|
|
) == "arrow.up.arrow.down"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func onlyRunningTransfersChooseTheDirectionSymbol() {
|
|
let transfers = [
|
|
transfer(direction: .upload, state: .running),
|
|
transfer(direction: .download, state: .queued),
|
|
transfer(direction: .download, state: .paused),
|
|
transfer(direction: .download, state: .completed),
|
|
transfer(direction: .download, state: .cancelled),
|
|
transfer(direction: .download, state: .failed("Offline")),
|
|
]
|
|
|
|
#expect(StatusBarTransferPresentation.runningDirectionSymbol(for: transfers) == "arrow.up")
|
|
#expect(
|
|
StatusBarTransferPresentation.runningDirectionSymbol(
|
|
for: [transfer(direction: .download, state: .queued)]
|
|
) == "arrow.up.arrow.down"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func uploadAndDownloadRoutesHideLocalPathsInEveryState() {
|
|
let source = Host(name: "Source", host: "source.example")
|
|
let destination = Host(name: "Destination", host: "destination.example")
|
|
let names = [source.id: source.name, destination.id: destination.name]
|
|
let states: [SFTPTransfer.State] = [
|
|
.queued, .running, .paused, .completed, .cancelled, .failed("Offline"),
|
|
]
|
|
|
|
for state in states {
|
|
let upload = SFTPTransfer(
|
|
sourceHostID: nil,
|
|
destinationHostID: destination.id,
|
|
direction: .upload,
|
|
localPath: "/Users/example/private/upload.txt",
|
|
remotePath: "/home/deploy/releases/upload.txt",
|
|
destinationHomeDirectory: "/home/deploy",
|
|
state: state
|
|
)
|
|
let download = SFTPTransfer(
|
|
sourceHostID: source.id,
|
|
destinationHostID: nil,
|
|
direction: .download,
|
|
localPath: "/Users/example/private/download.txt",
|
|
remotePath: "/home/deploy/backups/download.txt",
|
|
sourceHomeDirectory: "/home/deploy",
|
|
state: state
|
|
)
|
|
|
|
let uploadRoute = StatusBarTransferPresentation.route(for: upload) { names[$0] }
|
|
let downloadRoute = StatusBarTransferPresentation.route(for: download) { names[$0] }
|
|
|
|
#expect(uploadRoute == "Destination · ~/releases/upload.txt")
|
|
#expect(downloadRoute == "Source · ~/backups/download.txt")
|
|
#expect(!uploadRoute.contains(upload.localPath))
|
|
#expect(!downloadRoute.contains(download.localPath))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func remoteCopyRoutesStillShowBothRemoteEndpoints() {
|
|
let source = Host(name: "Source", host: "source.example")
|
|
let destination = Host(name: "Destination", host: "destination.example")
|
|
let names = [source.id: source.name, destination.id: destination.name]
|
|
let copy = SFTPTransfer(
|
|
sourceHostID: source.id,
|
|
destinationHostID: destination.id,
|
|
direction: .copy,
|
|
localPath: "/home/source/in/file.txt",
|
|
remotePath: "/home/destination/out/file.txt",
|
|
sourceHomeDirectory: "/home/source",
|
|
destinationHomeDirectory: "/home/destination"
|
|
)
|
|
|
|
#expect(
|
|
StatusBarTransferPresentation.route(for: copy) { names[$0] }
|
|
== "Source · ~/in/file.txt → Destination · ~/out/file.txt"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func transferRoutesFallBackToTheDisplayedRemotePathForUnknownHosts() {
|
|
let upload = SFTPTransfer(
|
|
destinationHostID: UUID(),
|
|
direction: .upload,
|
|
localPath: "/Users/example/private/file.txt",
|
|
remotePath: "/home/deploy/file.txt",
|
|
destinationHomeDirectory: "/home/deploy"
|
|
)
|
|
|
|
#expect(StatusBarTransferPresentation.route(for: upload) { _ in nil } == "~/file.txt")
|
|
}
|
|
|
|
private func transfer(
|
|
direction: SFTPTransfer.Direction,
|
|
state: SFTPTransfer.State,
|
|
byteCount: Int64 = 0,
|
|
transferredBytes: Int64 = 0
|
|
) -> SFTPTransfer {
|
|
SFTPTransfer(
|
|
direction: direction,
|
|
localPath: "/tmp/file.txt",
|
|
remotePath: "/srv/file.txt",
|
|
byteCount: byteCount,
|
|
transferredBytes: transferredBytes,
|
|
state: state
|
|
)
|
|
}
|
|
}
|