The body editor yields a paste it cannot read — the screenshot's ⌘V reaches the card window's attachment branch
The editor holds the keyboard from the moment the card window opens, and NSTextView answers an image-only pasteboard with a disabled Paste row — so the window's image branch sat one responder below the keyboard and could never be reached by it. The yield is by capability (readablePasteboardTypes), the same shape as the editor's existing refusal of file drops: any text flavor keeps the paste in the editor, and a pasteboard the editor has no reading of passes paste: to the responder behind it. Live-probed with real window-server ⌘V chords in both windows. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
@@ -769,3 +770,90 @@ struct PasteBoardBackgroundTests {
|
||||
== "Pasted Background.png")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The body editor's paste yield
|
||||
|
||||
/// The responder behind the editor in these tests — stands where the card window's
|
||||
/// `cardWindowImagePaste` bridge stands in the app, and only counts.
|
||||
@MainActor
|
||||
private final class PasteCatcher: NSView {
|
||||
var pastes = 0
|
||||
@objc func paste(_ sender: Any?) { pastes += 1 }
|
||||
}
|
||||
|
||||
/// **`CardBodyTextView` yields a paste it cannot read** (05-card-window.md ▸ Attachments, ruled
|
||||
/// 2026-08-09) — the screenshot pasteboard reaches the window's attachment branch even while the
|
||||
/// editor holds the keyboard, and a text paste never leaves the editor.
|
||||
///
|
||||
/// The pasteboard is a private named one through the view's `yieldPasteboard` seam, so the suite
|
||||
/// never reads the machine's — except through `super.paste`, which is AppKit's own and is exactly
|
||||
/// why the text-path test asserts the catcher stayed silent rather than what landed in the view.
|
||||
@MainActor
|
||||
@Suite("Paste yield ▸ the body editor")
|
||||
struct PasteYieldTests {
|
||||
|
||||
private func makeEditor(behind catcher: PasteCatcher? = nil) -> (CardBodyTextView, NSPasteboard) {
|
||||
let pasteboard = NSPasteboard(name: NSPasteboard.Name("test-yield-\(UUID().uuidString)"))
|
||||
pasteboard.clearContents()
|
||||
let editor = CardBodyTextView(frame: .zero)
|
||||
editor.isRichText = false
|
||||
editor.isEditable = true
|
||||
editor.yieldPasteboard = pasteboard
|
||||
catcher?.addSubview(editor)
|
||||
return (editor, pasteboard)
|
||||
}
|
||||
|
||||
private var pasteItem: NSMenuItem {
|
||||
NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "")
|
||||
}
|
||||
|
||||
@Test("An image-only pasteboard validates through the editor and forwards to the responder behind")
|
||||
func imageOnlyYields() {
|
||||
let catcher = PasteCatcher(frame: .zero)
|
||||
let (editor, pasteboard) = makeEditor(behind: catcher)
|
||||
defer { pasteboard.releaseGlobally() }
|
||||
pasteboard.setData(encodedImage(.png), forType: .png)
|
||||
|
||||
#expect(editor.validateUserInterfaceItem(pasteItem))
|
||||
editor.paste(nil)
|
||||
#expect(catcher.pastes == 1)
|
||||
}
|
||||
|
||||
/// No expectation on `validateUserInterfaceItem` here: a readable pasteboard routes validation
|
||||
/// to `super`, and `NSTextView`'s own answer reads the *machine's* general pasteboard — asserting
|
||||
/// it would tie the test to whatever the host's clipboard happens to hold. The claim under test
|
||||
/// is the routing: a text flavor means the paste is the editor's, so nothing is forwarded.
|
||||
@Test("A text flavor keeps the paste in the editor — riding image or not")
|
||||
func textStaysTheEditors() {
|
||||
let catcher = PasteCatcher(frame: .zero)
|
||||
let (editor, pasteboard) = makeEditor(behind: catcher)
|
||||
defer { pasteboard.releaseGlobally() }
|
||||
pasteboard.setString("plain words", forType: .string)
|
||||
pasteboard.setData(encodedImage(.png), forType: .png)
|
||||
|
||||
editor.paste(nil)
|
||||
#expect(catcher.pastes == 0)
|
||||
}
|
||||
|
||||
@Test("Preview mode takes no paste at all, so the window's branch owns it outright")
|
||||
func previewYieldsEverything() {
|
||||
let catcher = PasteCatcher(frame: .zero)
|
||||
let (editor, pasteboard) = makeEditor(behind: catcher)
|
||||
defer { pasteboard.releaseGlobally() }
|
||||
editor.isEditable = false
|
||||
pasteboard.setData(encodedImage(.png), forType: .png)
|
||||
|
||||
#expect(editor.validateUserInterfaceItem(pasteItem))
|
||||
editor.paste(nil)
|
||||
#expect(catcher.pastes == 1)
|
||||
}
|
||||
|
||||
@Test("Nothing behind to take it means a disabled row, not a swallowed gesture")
|
||||
func noTargetDisables() {
|
||||
let (editor, pasteboard) = makeEditor()
|
||||
defer { pasteboard.releaseGlobally() }
|
||||
pasteboard.setData(encodedImage(.png), forType: .png)
|
||||
|
||||
#expect(!editor.validateUserInterfaceItem(pasteItem))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user