207 lines
6.8 KiB
Swift
207 lines
6.8 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AppKit
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite
|
|
@MainActor
|
|
struct WorkspaceLayoutDragSourceTests {
|
|
private let sourceFrame = CGRect(x: 100, y: 100, width: 600, height: 400)
|
|
private let otherFrame = CGRect(x: 800, y: 100, width: 600, height: 400)
|
|
private let outside = CGPoint(x: 740, y: 260)
|
|
|
|
@Test
|
|
func releasingOutsideTheSourceTearsOffAndClearsTheLivePayload() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
var received: CGPoint?
|
|
source.onTearOff = {
|
|
received = $0
|
|
#expect(model.dragging == source.payload)
|
|
}
|
|
|
|
#expect(source.beginTracking())
|
|
finish(source, at: outside)
|
|
|
|
#expect(received == outside)
|
|
#expect(model.dragging == nil)
|
|
finish(source, at: outside)
|
|
#expect(received == outside)
|
|
}
|
|
|
|
@Test
|
|
func releasingWithinTheSourceWindowDoesNotTearOff() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
source.onTearOff = { _ in Issue.record("A release inside the source window must not create a window") }
|
|
#expect(source.beginTracking())
|
|
|
|
finish(source, at: CGPoint(x: sourceFrame.midX, y: sourceFrame.midY))
|
|
|
|
#expect(model.dragging == nil)
|
|
}
|
|
|
|
@Test
|
|
func anUnhandledReleaseOverAnotherWindowStillTearsOff() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
let point = CGPoint(x: otherFrame.midX, y: otherFrame.midY)
|
|
var received: CGPoint?
|
|
source.onTearOff = { received = $0 }
|
|
#expect(source.beginTracking())
|
|
|
|
finish(source, at: point)
|
|
|
|
#expect(received == point)
|
|
#expect(model.dragging == nil)
|
|
}
|
|
|
|
@Test
|
|
func acceptedDropsAndCancelledDragsNeverTearOff() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
source.onTearOff = { _ in Issue.record("An accepted or cancelled drag must not create a window") }
|
|
|
|
#expect(source.beginTracking())
|
|
finish(source, at: outside, operation: .move)
|
|
#expect(model.dragging == nil)
|
|
|
|
#expect(source.beginTracking())
|
|
finish(source, at: outside, primaryButtonPressed: true)
|
|
#expect(model.dragging == nil)
|
|
|
|
#expect(source.beginTracking())
|
|
finish(source, at: outside, currentEventIsEscape: true)
|
|
#expect(model.dragging == nil)
|
|
}
|
|
|
|
@Test
|
|
func consumedOrReplacedPayloadDoesNotTearOffOrClearAnotherDrag() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
source.onTearOff = { _ in Issue.record("A consumed payload cannot be moved again") }
|
|
|
|
#expect(source.beginTracking())
|
|
model.dragging = nil
|
|
finish(source, at: outside)
|
|
#expect(model.dragging == nil)
|
|
|
|
#expect(source.beginTracking())
|
|
let replacement = DragPayload.pane(UUID())
|
|
model.dragging = replacement
|
|
finish(source, at: outside)
|
|
#expect(model.dragging == replacement)
|
|
}
|
|
|
|
@Test
|
|
func sourceStaysTransparentAndRemovesItsRecognizerWhenDetached() {
|
|
let window = NSWindow(
|
|
contentRect: CGRect(x: 0, y: 0, width: 300, height: 200),
|
|
styleMask: .borderless, backing: .buffered, defer: false
|
|
)
|
|
window.isReleasedWhenClosed = false
|
|
defer { window.close() }
|
|
let root = NSView(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
|
|
window.contentView = root
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
source.frame = CGRect(x: 0, y: 0, width: 200, height: 32)
|
|
root.addSubview(source)
|
|
|
|
#expect(!window.isVisible)
|
|
#expect(source.hitTest(CGPoint(x: 10, y: 10)) == nil)
|
|
#expect(source.panRecognizer.view === root)
|
|
#expect(!source.panRecognizer.delaysPrimaryMouseButtonEvents)
|
|
#expect(!source.panRecognizer.delaysSecondaryMouseButtonEvents)
|
|
#expect(!source.panRecognizer.delaysKeyEvents)
|
|
#expect(source.beginTracking())
|
|
|
|
source.removeFromSuperview()
|
|
|
|
#expect(source.panRecognizer.view == nil)
|
|
#expect(root.gestureRecognizers.isEmpty)
|
|
#expect(model.dragging == nil)
|
|
}
|
|
|
|
@Test
|
|
func hiddenSourcesAndTrailingControlsCannotStartDrags() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
source.frame = CGRect(x: 0, y: 0, width: 200, height: 32)
|
|
source.excludedTrailingWidth = 30
|
|
|
|
#expect(source.acceptsDragStart(at: CGPoint(x: 20, y: 16)))
|
|
#expect(!source.acceptsDragStart(at: CGPoint(x: 185, y: 16)))
|
|
#expect(!source.acceptsDragStart(at: CGPoint(x: 20, y: 40)))
|
|
|
|
source.isHidden = true
|
|
|
|
#expect(!source.acceptsDragStart(at: CGPoint(x: 20, y: 16)))
|
|
}
|
|
|
|
@Test
|
|
func parkedPaneSourcesCannotStartDragsOverTheSelectedWorkspaceOrVault() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
let workspace = model.workspaces[0]
|
|
source.payload = .pane(workspace.focusedPaneID)
|
|
#expect(source.isEligible)
|
|
model.open(Workspace.sshSession(host: Host(name: "Other", host: "other.example.invalid")))
|
|
|
|
#expect(!source.isEligible)
|
|
#expect(!source.beginTracking())
|
|
|
|
model.select(workspace.id)
|
|
#expect(source.isEligible)
|
|
|
|
model.selectVault()
|
|
#expect(!source.isEligible)
|
|
#expect(!source.beginTracking())
|
|
}
|
|
|
|
@Test
|
|
func workspaceSourceRequiresTheVisibleNavigator() {
|
|
let model = WindowModel()
|
|
let source = source(model: model)
|
|
#expect(source.isEligible)
|
|
|
|
model.sidebarMode = .assistant
|
|
#expect(!source.isEligible)
|
|
|
|
model.sidebarMode = .navigation
|
|
model.sidebar.isPresented = false
|
|
#expect(!source.isEligible)
|
|
|
|
model.sidebar.isPresented = true
|
|
#expect(source.isEligible)
|
|
model.closeForWindow()
|
|
#expect(!source.isEligible)
|
|
}
|
|
|
|
private func source(model: WindowModel) -> WorkspaceLayoutDragSourceNSView {
|
|
let workspace = model.open(Workspace.sshSession(host: Host(name: "Source", host: "source.example.invalid")))
|
|
let source = WorkspaceLayoutDragSourceNSView()
|
|
source.model = model
|
|
source.payload = .workspace(workspace.id)
|
|
return source
|
|
}
|
|
|
|
private func finish(
|
|
_ source: WorkspaceLayoutDragSourceNSView,
|
|
at point: CGPoint,
|
|
operation: NSDragOperation = [],
|
|
primaryButtonPressed: Bool = false,
|
|
currentEventIsEscape: Bool = false
|
|
) {
|
|
source.finishTracking(
|
|
at: point, operation: operation,
|
|
primaryButtonPressed: primaryButtonPressed, currentEventIsEscape: currentEventIsEscape,
|
|
windowFrames: [sourceFrame]
|
|
)
|
|
}
|
|
}
|