Files
Termind/TFMTests/SFTPContextMenuTests.swift

175 lines
8.1 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AppKit
import SwiftUI
import TermindDomain
import Testing
@testable import Termind
@Suite(.serialized) @MainActor
struct SFTPContextMenuTests {
@Test func singleFileKeepsItsActionsAndOpenWithApplicationIcons() throws {
let file = file("config.json")
let icon = NSImage(size: NSSize(width: 16, height: 16))
let application = SFTPFileApplication(
url: URL(filePath: "/Applications/Editor.app"), name: "Editor", icon: icon, isDefault: true)
var actions: [SFTPFileContextMenu.Action] = []
let entries = snapshot(file).entries(applications: { _ in [application] }, perform: { actions.append($0) })
let menu = NativeMenuEntry.menu(entries)
#expect(
menu.items.map(\.title) == [
"Open", "Open With", "Download", "", "Copy", "Copy as", "Paste", "Rename",
"Edit Permissions", "Delete", "", "New Folder", "Upload", "Refresh",
])
#expect(!menu.autoenablesItems)
let openWith = try #require(menu.items[1].submenu)
#expect(openWith.items.map(\.title) == ["Editor (default)", "", "App Store…", "Other…"])
#expect(openWith.items[0].image === icon)
openWith.performActionForItem(at: 0)
menu.performActionForItem(at: 9)
#expect(actions == [.openWith(application.url), .delete])
#expect(menu.items[9].image != nil)
if #available(macOS 27.0, *) {
#expect(menu.items[0].preferredImageVisibility == .automatic)
#expect(menu.items[9].preferredImageVisibility == .automatic)
}
let copyAs = try #require(menu.items[5].submenu)
#expect(copyAs.items.map(\.title) == ["Name", "Path"])
#expect(copyAs.items.allSatisfy { $0.image == nil })
}
@Test func directoriesAndMultipleSelectionsDoNotDiscoverApplications() {
let directory = file("folder", kind: .directory)
let first = file("a.txt")
let second = file("b.txt")
var discoveries = 0
let applications: @MainActor (String) -> [SFTPFileApplication] = { _ in
discoveries += 1
return []
}
let folderMenu = snapshot(directory).entries(applications: applications) { _ in }
#expect(folderMenu.contains { $0.title == "Open" })
#expect(!folderMenu.contains { $0.title == "Open With" })
#expect(folderMenu.contains { $0.title == "Edit Permissions" })
let multiple = snapshot(first, targets: [first, second]).entries(applications: applications) { _ in }
#expect(
multiple.map(\.title) == [
"Download 2 Items", "", "Copy 2 Items", "Copy as", "Paste", "Delete 2 Items", "",
"New Folder", "Upload", "Refresh",
])
#expect(multiple[3].children.map(\.title) == ["Names", "Paths"])
#expect(discoveries == 0)
}
@Test func connectionClipboardAndRefreshStateKeepTheirDisabledActions() throws {
let disconnected = snapshot(file("README"), connected: false)
.entries(applications: { _ in [] }, perform: { _ in })
for title in ["Open With", "Paste", "Rename", "Edit Permissions", "Delete", "New Folder", "Upload", "Refresh"] {
#expect(try #require(disconnected.first { $0.title == title }).isEnabled == false)
}
let openWith = try #require(disconnected.first { $0.title == "Open With" })
#expect(openWith.children.map(\.title) == ["App Store…", "Other…"])
#expect(!openWith.children[0].isEnabled)
#expect(openWith.children[1].isEnabled)
let loading = snapshot(file("a.txt"), canPaste: false, isLoading: true)
.entries(applications: { _ in [] }, perform: { _ in })
#expect(try #require(loading.first { $0.title == "Paste" }).isEnabled == false)
#expect(try #require(loading.first { $0.title == "Refresh" }).isEnabled == false)
#expect(try #require(loading.first { $0.title == "Rename" }).isEnabled)
}
@Test(arguments: [
("en", "Download 2 Items", "Copy 2 Items", "Delete 2 Items"),
("zh-Hans", "下载2项", "复制2项", "删除2项"),
("zh-Hant", "下載2項", "複製2項", "刪除2項"),
])
func countedTitlesFollowTheAppLocale(_ language: String, _ download: String, _ copy: String, _ delete: String) {
let first = file("a.txt")
let menu = snapshot(first, targets: [first, file("b.txt")], locale: language)
.entries(applications: { _ in [] }, perform: { _ in })
#expect(menu[0].title == download)
#expect(menu[2].title == copy)
#expect(menu[5].title == delete)
}
@Test func rowReconfigurationDoesNotRebuildOrRetargetAnExistingMenu() throws {
var target = "a.txt"
var menuBuilds = 0
var performed: [String] = []
let row = HostingListRow(frame: CGRect(x: 0, y: 0, width: 400, height: 48))
func content() -> AnyView {
AnyView(
Text(target).frame(width: 400, height: 48)
.overlay {
NativeRowContextMenu(
entries: {
menuBuilds += 1
let snapshot = target
return [NativeMenuEntry(title: snapshot) { performed.append(snapshot) }]
}, isPresented: .constant(false))
})
}
row.configure(rootView: content())
row.layoutSubtreeIfNeeded()
let source = try #require(findMenuView(row))
let menu = try #require(source.makeMenu())
let item = try #require(menu.items.first)
target = "b.txt"
row.configure(rootView: content())
row.layoutSubtreeIfNeeded()
#expect(menuBuilds == 1)
#expect(menu.items.first === item)
#expect(item.title == "a.txt")
menu.performActionForItem(at: 0)
#expect(performed == ["a.txt"])
let nextMenu = try #require(findMenuView(row)?.makeMenu())
#expect(nextMenu !== menu)
#expect(nextMenu.items.first?.title == "b.txt")
#expect(menuBuilds == 2)
row.prepareForReuse()
nextMenu.performActionForItem(at: 0)
#expect(performed == ["a.txt", "b.txt"])
}
@Test func onlyContextClicksAreClaimedByTheMenuOverlay() throws {
let cases: [(NSEvent.EventType, NSEvent.ModifierFlags, Bool)] = [
(.rightMouseDown, [], true), (.rightMouseUp, [], true),
(.leftMouseDown, .control, true), (.leftMouseUp, .control, true),
(.leftMouseDown, [], false), (.leftMouseUp, [], false),
(.leftMouseDown, .command, false), (.leftMouseDown, .shift, false),
(.leftMouseDragged, [], false), (.mouseMoved, [], false),
]
for (type, modifiers, expected) in cases {
let event = try #require(
NSEvent.mouseEvent(
with: type, location: .zero, modifierFlags: modifiers, timestamp: 0,
windowNumber: 0, context: nil, eventNumber: 0, clickCount: 1, pressure: 0))
#expect(NativeRowContextMenu.RowMenuView.isContextMenuEvent(event) == expected)
}
}
private func file(_ name: String, kind: RemoteFile.Kind = .file) -> RemoteFile {
RemoteFile(
path: "/home/\(name)", name: name, kind: kind,
size: 10, permissions: 0o644, ownerUID: 0, ownerGID: 0)
}
private func snapshot(
_ file: RemoteFile, targets: [RemoteFile]? = nil, connected: Bool = true,
canPaste: Bool = true, isLoading: Bool = false, locale: String = "en"
) -> SFTPFileContextMenu {
SFTPFileContextMenu(
file: file, targets: targets ?? [file], connected: connected, allowsRemoteMutations: connected,
canPaste: canPaste, isLoading: isLoading, locale: Locale(identifier: locale))
}
private func findMenuView(_ view: NSView) -> NativeRowContextMenu.RowMenuView? {
if let menu = view as? NativeRowContextMenu.RowMenuView { return menu }
return view.subviews.lazy.compactMap(findMenuView).first
}
}