Files
clipbored/tests/clipboredtests/ClipboardPanelViewTests.swift

1558 lines
63 KiB
Swift
Raw Normal View History

2026-06-30 01:12:19 -07:00
import AppKit
import XCTest
@testable import ClipBored
final class ClipboardPanelViewTests: XCTestCase {
private var tempURLs: [URL] = []
private struct PanelFixture {
let window: NSWindow
let view: ClipboardPanelView
let viewModel: ClipboardPanelViewModel
let settings: SettingsModel
let store: ClipboardStore
let cacheService: ClipboardCacheService
let previewProbe: PreviewProbe
}
private final class PreviewProbe {
var requestCount = 0
2026-06-30 01:12:19 -07:00
}
override func tearDown() {
tempURLs.forEach { try? FileManager.default.removeItem(at: $0) }
tempURLs.removeAll()
super.tearDown()
}
func testSearchFieldEditingWhenSearchFieldIsFirstResponder() {
let (window, view) = makePanelWithPanelView()
window.makeFirstResponder(view)
XCTAssertFalse(view.isSearchFieldEditing)
view.focusSearchField()
XCTAssertTrue(view.isSearchFieldEditing)
}
func testSearchFieldEditingWhenFieldEditorIsFirstResponder() {
let (window, view) = makePanelWithPanelView()
view.focusSearchField()
guard let editor = window.fieldEditor(false, for: nil) else {
return XCTFail("Expected a search field editor")
}
window.makeFirstResponder(editor)
XCTAssertTrue(view.isSearchFieldEditing)
}
2026-07-01 14:39:36 -07:00
func testShelfChromeKeepsSearchAndCollectionsInOneCompactRow() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Compact shelf chrome", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugShelfChromeRowCount, 1)
XCTAssertTrue(fixture.view.debugShelfChromeContainsSearchAndCollections)
XCTAssertEqual(fixture.view.debugSearchFieldWidth, 34, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugSearchFieldPlaceholderText, "")
fixture.view.debugSetSearchFieldText("type:text")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugSearchFieldWidth, 300, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugSearchFieldPlaceholderText, "Search clips")
}
2026-06-30 01:12:19 -07:00
func testCapturedTextItemCreatesVisibleCardDocument() {
let fixture = makePanelFixture()
let item = makeTextItem("Bruh it said copied text but it does not appear.", store: fixture.store)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.id), [item.id])
XCTAssertEqual(fixture.view.debugVisibleCardCount, 1)
XCTAssertEqual(fixture.view.debugResultCountText, "1 clip")
XCTAssertTrue(fixture.view.debugDocumentViewIsCardStack)
XCTAssertGreaterThanOrEqual(fixture.view.debugDocumentViewFrame.width, 292)
XCTAssertGreaterThanOrEqual(fixture.view.debugDocumentViewFrame.height, 244)
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["text-preview"])
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, false])
2026-06-30 01:12:19 -07:00
}
2026-06-30 10:44:11 -07:00
func testSingleLineTextCardsDoNotDuplicateTitleInBody() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Client follow-up note", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardTextPreviewTitles, ["Client follow-up note"])
XCTAssertEqual(fixture.view.debugCardTextPreviewBodies, [""])
}
func testMultiLineTextCardsShowRemainderBelowTitle() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Address:\n399 The Embarcadero\nSan Francisco", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardTextPreviewTitles, ["Address:"])
XCTAssertEqual(fixture.view.debugCardTextPreviewBodies, ["399 The Embarcadero San Francisco"])
}
2026-06-30 03:11:40 -07:00
func testCompactCardsFitTwoItemsOnNarrowDockShelf() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
fixture.store.upsert(makeTextItem("Compact first", store: fixture.store))
fixture.store.upsert(makeTextItem("Compact second", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardDensity, "compact")
XCTAssertEqual(fixture.view.debugVisibleCardCount, 2)
XCTAssertEqual(fixture.view.debugCardSizes.count, 2)
XCTAssertEqual(fixture.view.debugCardSizes.first?.width ?? 0, 264, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugCardSizes.first?.height ?? 0, 220, accuracy: 0.5)
XCTAssertLessThanOrEqual(
fixture.view.debugDocumentViewFrame.width,
fixture.view.debugCardRailVisibleRect.width + 1
)
}
2026-06-30 02:38:48 -07:00
func testCardsShowQuickPasteNumberBadgesForFirstNineItems() {
let fixture = makePanelFixture()
for index in 0..<10 {
fixture.store.upsert(makeTextItem("Quick paste badge \(index)", store: fixture.store))
drainMainQueue()
}
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugVisibleCardCount, 10)
XCTAssertEqual(fixture.view.debugQuickPasteBadgeTexts, ["1", "2", "3", "4", "5", "6", "7", "8", "9"])
}
2026-06-30 01:12:19 -07:00
func testFooterShowsCaptureStatusInsteadOfShortcutInstructions() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Footer status item", store: fixture.store))
drainMainQueue()
XCTAssertEqual(fixture.view.debugStatusText, "Capture running")
XCTAssertEqual(fixture.view.debugStatusTone, "ready")
XCTAssertFalse(fixture.view.debugStatusText.contains("Enter paste"))
}
2026-06-30 10:14:25 -07:00
func testCardFooterHidesMissingSourceInsteadOfShowingUnknown() {
let fixture = makePanelFixture()
var item = makeTextItem("No source noise", store: fixture.store)
item.sourceApp = nil
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFirstCardFooterSourceIsHidden)
XCTAssertEqual(fixture.view.debugFirstCardFooterSourceText, "")
XCTAssertEqual(fixture.view.debugFirstCardFooterDetailText, "15 characters")
}
func testCardFooterShowsSourceAndUsageWhenUsed() {
let fixture = makePanelFixture()
var item = makeTextItem("Frequently pasted", store: fixture.store)
item.useCount = 3
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertFalse(fixture.view.debugFirstCardFooterSourceIsHidden)
XCTAssertEqual(fixture.view.debugFirstCardFooterSourceText, "Ghostty - Used 3 times")
}
2026-06-30 01:56:40 -07:00
func testEditedTextStatusUsesActionTone() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Editable footer item", store: fixture.store))
drainMainQueue()
fixture.viewModel.updateSelectedText(to: "Edited footer item")
drainMainQueue()
XCTAssertEqual(fixture.view.debugStatusText, "Updated text clip")
XCTAssertEqual(fixture.view.debugStatusTone, "action")
}
2026-06-30 01:12:19 -07:00
func testSkippedCaptureStatusUsesWarningTone() {
let fixture = makePanelFixture()
fixture.settings.setCaptureStatus(message: "Skipped: Audio items are ignored in capture settings.")
drainMainQueue()
XCTAssertEqual(fixture.view.debugStatusText, "Skipped: Audio items are ignored in capture settings.")
XCTAssertEqual(fixture.view.debugStatusTone, "warning")
}
func testPanelShellRendersAsSquareDockedSurface() {
let (_, view) = makePanelWithPanelView()
drainMainQueue()
view.layoutSubtreeIfNeeded()
view.displayIfNeeded()
let rep = try! XCTUnwrap(view.bitmapImageRepForCachingDisplay(in: view.bounds))
rep.size = view.bounds.size
view.cacheDisplay(in: view.bounds, to: rep)
let width = rep.pixelsWide
let height = rep.pixelsHigh
func alphaAt(_ x: Int, _ y: Int) -> CGFloat {
rep.colorAt(x: x, y: y)?.alphaComponent ?? 0
}
XCTAssertEqual(view.debugPanelCornerRadius, 0)
XCTAssertGreaterThan(alphaAt(8, 8), 0.9)
XCTAssertGreaterThan(alphaAt(width - 9, 8), 0.9)
XCTAssertGreaterThan(alphaAt(8, height - 9), 0.9)
XCTAssertGreaterThan(alphaAt(width - 9, height - 9), 0.9)
}
func testOpeningTransitionDefersCardRailReloadUntilAnimationCompletes() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Existing clip", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugVisibleCardCount, 1)
fixture.view.beginOpeningTransition()
XCTAssertTrue(fixture.view.debugIsDeferringVisualReloads)
fixture.store.upsert(makeTextItem("New clip during open", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugVisibleCardCount, 1)
XCTAssertEqual(fixture.view.debugResultCountText, "2 clips")
fixture.view.finishOpeningTransition()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertFalse(fixture.view.debugIsDeferringVisualReloads)
XCTAssertEqual(fixture.view.debugVisibleCardCount, 2)
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels.first, "Text: New clip during open")
}
func testCollectionRailUsesPasteStyleLabelsAndTracksSelection() {
let fixture = makePanelFixture()
XCTAssertEqual(
fixture.view.debugCollectionTitles,
2026-07-01 15:39:43 -07:00
["Clipboard", "Frequent", "Text", "Links", "Images", "Colors", "Audio", "Videos", "Files", "Pinned", "Code"]
2026-06-30 01:12:19 -07:00
)
2026-06-30 16:39:41 -07:00
XCTAssertEqual(
fixture.view.debugCollectionLeadingSymbols,
2026-07-01 15:39:43 -07:00
["doc.on.clipboard", "chart.bar.fill", "text.alignleft", "link", "photo", "paintpalette", "music.note", "film", "doc.fill", "pin.fill", "chevron.left.forwardslash.chevron.right"]
2026-06-30 16:39:41 -07:00
)
2026-06-30 01:12:19 -07:00
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Clipboard")
fixture.viewModel.sortMode = .links
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links")
}
func testCollectionRailChipsAreKeyboardFocusableAndVoiceOverDescriptive() {
let fixture = makePanelFixture()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCollectionChipAcceptsFirstResponder, Array(repeating: true, count: ClipboardSortMode.allCases.count))
2026-06-30 10:39:33 -07:00
XCTAssertEqual(fixture.view.debugCollectionCountLabelHiddenStates, Array(repeating: true, count: ClipboardSortMode.allCases.count))
XCTAssertEqual(fixture.view.debugCollectionChipAccessibilityLabels.first, "Clipboard, selected, 0 clips")
XCTAssertTrue(fixture.view.debugFocusCollectionChip(.links))
fixture.view.debugPressFocusedResponderWithSpace()
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links")
XCTAssertTrue(fixture.view.debugCollectionChipAccessibilityLabels.contains("Links, selected, 0 clips"))
}
func testCollectionRailChipsSupportShelfNavigationKeys() {
let fixture = makePanelFixture()
var clientItem = makeTextItem("Client collection item", store: fixture.store)
clientItem.collectionName = "Client Work"
fixture.store.upsert(clientItem)
fixture.store.upsert(makeTextItem("Stack queue item", store: fixture.store))
drainMainQueue()
fixture.viewModel.selectItem(at: 0)
fixture.viewModel.toggleSelectedStackMembership()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCollectionChip(.links))
fixture.view.debugPressFocusedResponderKeyCode(124)
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Images")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCollectionTitles, ["Images"])
fixture.view.debugPressFocusedResponderKeyCode(123)
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCollectionTitles, ["Links"])
fixture.view.debugPressFocusedResponderKeyCode(119)
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Stack")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCollectionTitles, ["Stack"])
fixture.view.debugPressFocusedResponderKeyCode(123)
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Client Work")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCollectionTitles, ["Client Work"])
fixture.view.debugPressFocusedResponderKeyCode(115)
drainMainQueue()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Clipboard")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCollectionTitles, ["Clipboard"])
}
2026-06-30 09:40:04 -07:00
func testTypingFromFocusedCollectionChipStartsSearch() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store))
fixture.store.upsert(makeTextItem("Quantum reference", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCollectionChip(.links))
fixture.view.debugTypeFocusedResponder("q", keyCode: 12)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.isSearchFieldEditing)
XCTAssertEqual(fixture.view.debugSearchFieldText, "q")
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference"])
}
2026-06-30 09:44:45 -07:00
func testKeyboardCancelClearsSearchFromFocusedCollectionChip() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store))
fixture.store.upsert(makeTextItem("Quantum reference", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.view.debugSetSearchFieldText("quantum")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference"])
XCTAssertTrue(fixture.view.debugFocusCollectionChip(.text))
XCTAssertTrue(fixture.view.clearSearchForKeyboardCancel())
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.isSearchFieldEditing)
XCTAssertEqual(fixture.view.debugSearchFieldText, "")
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference", "Alpha note"])
}
2026-06-30 03:51:06 -07:00
func testCollectionRailAddButtonCreatesEmptyCollection() {
2026-06-30 01:34:05 -07:00
let fixture = makePanelFixture()
XCTAssertTrue(fixture.view.debugCollectionRailContainsAddButton)
XCTAssertTrue(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.view.debugSetCollectionNameProvider { " Research Stack " }
fixture.view.debugPressAddCollectionButton()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
2026-06-30 03:51:06 -07:00
XCTAssertEqual(fixture.viewModel.statusMessage, "Created Research Stack")
2026-06-30 01:34:05 -07:00
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Research Stack"])
2026-06-30 03:51:06 -07:00
XCTAssertEqual(fixture.view.debugCustomCollectionCounts, [0])
2026-06-30 10:39:33 -07:00
XCTAssertEqual(fixture.view.debugCustomCollectionCountLabelHiddenStates, [true])
2026-06-30 03:51:06 -07:00
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Research Stack")
XCTAssertEqual(fixture.view.debugVisibleCardCount, 0)
XCTAssertEqual(fixture.view.debugEmptyStateText?.title, "No clips in Research Stack")
XCTAssertEqual(fixture.view.debugEmptyStateText?.detail, "Drag clips here or use Collect to add them.")
}
2026-06-30 01:34:05 -07:00
2026-06-30 03:51:06 -07:00
func testCollectionFilteredCardsUseStoredCollectionHeaderColor() {
let fixture = makePanelFixture()
fixture.viewModel.createCollection(named: "Research Stack", colorHex: "#0A9EB8")
var item = makeTextItem("Collect this note", store: fixture.store)
item.collectionName = "Research Stack"
fixture.store.upsert(item)
2026-06-30 01:34:05 -07:00
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
2026-06-30 01:34:05 -07:00
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Collect this note"])
XCTAssertEqual(fixture.view.debugFirstCardHeaderTitle, "Research Stack")
XCTAssertEqual(fixture.view.debugFirstCardHeaderSubtitle, "Text - Just now")
2026-06-30 03:51:06 -07:00
XCTAssertEqual(fixture.view.debugFirstCardHeaderColorHex, "#0A9EB8")
XCTAssertEqual(fixture.view.debugCustomCollectionColorHexes["Research Stack"], "#0A9EB8")
XCTAssertEqual(fixture.view.debugFirstCardFooterDetailText, "17 characters")
2026-06-30 01:34:05 -07:00
}
2026-06-30 10:22:09 -07:00
func testCardHeaderUsesReadableRelativeAgeText() {
let fixture = makePanelFixture()
var item = makeTextItem("Readable age", store: fixture.store)
item.createdAt = Date().addingTimeInterval(-3 * 60)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugFirstCardHeaderSubtitle, "3 minutes ago")
fixture.viewModel.createCollection(named: "Age Stack", colorHex: "#FF3B30", selectAfterCreate: false)
fixture.viewModel.assignSelected(to: "Age Stack")
fixture.viewModel.selectCollection(named: "Age Stack")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugFirstCardHeaderSubtitle, "Text - 3 minutes ago")
}
2026-06-30 04:01:57 -07:00
func testCollectionChipsExposeManagementMenuActions() {
let fixture = makePanelFixture()
fixture.viewModel.createCollection(named: "Research Stack", colorHex: "#0A9EB8")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCustomCollectionMenuTitles(named: "Research Stack"), ["Edit Collection...", "-", "Delete Collection"])
}
func testCollectionChipManagementRenamesAndDeletesCollections() {
let fixture = makePanelFixture()
fixture.viewModel.createCollection(named: "Research Stack", colorHex: "#0A9EB8")
var item = makeTextItem("Collect this note", store: fixture.store)
item.collectionName = "Research Stack"
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.view.debugEditCollection(named: "Research Stack", to: "Product Research", colorHex: "#3366FF")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Product Research"])
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Product Research")
XCTAssertEqual(fixture.view.debugFirstCardHeaderTitle, "Product Research")
XCTAssertEqual(fixture.view.debugFirstCardHeaderColorHex, "#3366FF")
fixture.view.debugDeleteCollection(named: "Product Research")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, [])
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), [])
XCTAssertEqual(fixture.store.items.map(\.payload), [])
}
2026-06-30 01:12:19 -07:00
func testSelectedCardActionsRespectSelectedKind() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
2026-06-30 01:12:19 -07:00
2026-06-30 10:34:37 -07:00
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Edit", "Preview", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210)
2026-06-30 01:12:19 -07:00
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertFalse(fixture.view.debugFirstCardHeaderBadgeIsHidden)
2026-06-30 10:52:38 -07:00
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.width, 56, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.height, 56, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxX, 320, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxY, 244, accuracy: 0.5)
2026-06-30 01:12:19 -07:00
fixture.store.upsert(makeItem(kind: .file, text: "/tmp/report.txt", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
2026-06-30 01:12:19 -07:00
fixture.viewModel.selectFirstItem()
fixture.window.contentView?.layoutSubtreeIfNeeded()
2026-06-30 01:12:19 -07:00
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
2026-06-30 10:34:37 -07:00
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Paste Plain Text", "Collect", "Preview", "Open", "Reveal", "More"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
2026-06-30 01:12:19 -07:00
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertFalse(fixture.view.debugFirstCardHeaderBadgeIsHidden)
2026-06-30 10:52:38 -07:00
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.width, 56, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.height, 56, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxX, 320, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxY, 244, accuracy: 0.5)
2026-06-30 01:12:19 -07:00
}
func testCompactFileCardActionsFitInsideShelfWithOverflowMenu() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
fixture.store.upsert(makeItem(kind: .file, text: "/tmp/report.txt", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardDensity, "compact")
2026-06-30 10:34:37 -07:00
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Paste Plain Text", "Collect", "Preview", "Open", "More"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 196)
XCTAssertLessThanOrEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 197)
XCTAssertFalse(fixture.view.debugFirstCardHeaderBadgeIsHidden)
2026-06-30 10:52:38 -07:00
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.width, 50, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.height, 50, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxX, 264, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugFirstCardHeaderBadgeFrame.maxY, 220, accuracy: 0.5)
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
2026-07-01 15:20:13 -07:00
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Rename...", "Add to Stack", "Add Visible Clips to Stack", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
)
}
func testCardsAreKeyboardFocusableAndReturnPastesFocusedCard() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Older text card", store: fixture.store))
fixture.store.upsert(makeTextItem("Newest text card", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAcceptsFirstResponder, [true, true])
XCTAssertTrue(fixture.view.debugFocusCard(at: 1))
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedItem?.payload, "Older text card")
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [1])
XCTAssertEqual(fixture.view.debugCardBorderWidths[1], 2)
XCTAssertEqual(fixture.view.debugCardAccessibilityValues[1], "Selected")
2026-06-30 09:33:00 -07:00
XCTAssertEqual(fixture.view.debugCardAccessibilityHelps[1], "Press Return to paste. Press Space for Quick Look.")
fixture.view.debugPressFocusedResponderWithReturn()
drainMainQueue()
XCTAssertEqual(fixture.viewModel.statusMessage, "Copied")
}
2026-06-30 09:33:00 -07:00
func testFocusedTextCardSpaceOpensQuickLookInsteadOfPasting() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Preview this text without pasting", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
drainMainQueue()
fixture.view.debugPressFocusedResponderWithSpace()
drainMainQueue()
XCTAssertEqual(fixture.previewProbe.requestCount, 1)
XCTAssertEqual(fixture.viewModel.selectedItem?.payload, "Preview this text without pasting")
XCTAssertEqual(fixture.viewModel.statusMessage, "")
}
2026-06-30 09:40:04 -07:00
func testTypingFromFocusedCardStartsSearch() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store))
fixture.store.upsert(makeTextItem("Quantum card", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
fixture.view.debugTypeFocusedResponder("q", keyCode: 12)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.isSearchFieldEditing)
XCTAssertEqual(fixture.view.debugSearchFieldText, "q")
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card"])
}
2026-06-30 09:44:45 -07:00
func testKeyboardCancelClearsSearchFromFocusedCard() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store))
fixture.store.upsert(makeTextItem("Quantum card", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.view.debugSetSearchFieldText("quantum")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card"])
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
XCTAssertTrue(fixture.view.clearSearchForKeyboardCancel())
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.isSearchFieldEditing)
XCTAssertEqual(fixture.view.debugSearchFieldText, "")
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card", "Alpha note"])
XCTAssertFalse(fixture.view.clearSearchForKeyboardCancel())
}
func testFocusedPreviewableCardSpaceOpensQuickLook() {
let fixture = makePanelFixture()
fixture.store.upsert(makeItem(kind: .url, text: "https://example.com/read", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
drainMainQueue()
XCTAssertEqual(fixture.view.debugCardAccessibilityHelps.first, "Press Return to paste. Press Space for Quick Look.")
fixture.view.debugPressFocusedResponderWithSpace()
drainMainQueue()
XCTAssertEqual(fixture.previewProbe.requestCount, 1)
XCTAssertEqual(fixture.viewModel.selectedItem?.payload, "https://example.com/read")
}
2026-06-30 09:19:18 -07:00
func testFocusedCardsSupportShelfNavigationKeys() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
for index in 0..<8 {
fixture.store.upsert(makeTextItem("Keyboard navigation item \(index)", store: fixture.store))
drainMainQueue()
}
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.selectFirstItem()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
let pageStep = fixture.view.debugVisibleCardPageStep
XCTAssertGreaterThan(pageStep, 1)
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
fixture.view.debugPressFocusedResponderKeyCode(124)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, 1)
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [1])
fixture.view.debugPressFocusedResponderKeyCode(121)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, min(7, 1 + pageStep))
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [fixture.viewModel.selectedIndex])
fixture.view.debugPressFocusedResponderKeyCode(119)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, 7)
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [7])
fixture.view.debugPressFocusedResponderKeyCode(116)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, max(0, 7 - pageStep))
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [fixture.viewModel.selectedIndex])
fixture.view.debugPressFocusedResponderKeyCode(115)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, 0)
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [0])
fixture.view.debugPressFocusedResponderKeyCode(123)
drainMainQueue()
XCTAssertEqual(fixture.viewModel.selectedIndex, 0)
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [0])
}
2026-06-30 01:12:19 -07:00
func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable() {
let fixture = makePanelFixture()
var item = makeItem(kind: .url, text: "https://example.com", store: fixture.store)
item.sourceApp = nil
fixture.store.upsert(item)
2026-06-30 01:12:19 -07:00
drainMainQueue()
XCTAssertEqual(fixture.view.debugCardHeaderBadgeSymbols, ["link"])
XCTAssertEqual(fixture.view.debugCardHeaderBadgeTexts, [""])
}
func testCardHeaderUsesSourceMonogramWhenAppIconIsUnavailable() {
let fixture = makePanelFixture()
var item = makeTextItem("Copied from a named app", store: fixture.store)
item.sourceApp = "Arc Browser"
fixture.store.upsert(item)
drainMainQueue()
XCTAssertEqual(fixture.view.debugCardHeaderBadgeTexts, ["AB"])
2026-06-30 01:12:19 -07:00
}
func testCollectionRailShowsLiveCounts() {
let fixture = makePanelFixture()
var pinned = makeTextItem("Pinned note", store: fixture.store)
pinned.isPinned = true
let rich = makeItem(kind: .richText, text: "Rich note", store: fixture.store)
let link = makeItem(kind: .url, text: "https://example.com/releases", store: fixture.store)
let image = makeItem(kind: .image, text: "image payload", store: fixture.store)
2026-07-01 14:52:54 -07:00
let color = makeItem(kind: .color, displayText: "#0A84FF", payload: "#0A84FF", store: fixture.store)
2026-06-30 01:12:19 -07:00
let audio = makeItem(kind: .audio, text: "audio payload", store: fixture.store)
2026-07-01 15:39:43 -07:00
let video = makeItem(kind: .video, text: "/tmp/movie.mp4", store: fixture.store)
2026-06-30 01:12:19 -07:00
let file = makeItem(kind: .file, text: "/tmp/report.pdf", store: fixture.store)
2026-07-01 15:12:49 -07:00
let code = makeItem(
kind: .code,
displayText: "Swift Snippet",
payload: "func greet(name: String) -> String {\n return \"Hi \\(name)\"\n}",
store: fixture.store
)
2026-06-30 01:12:19 -07:00
2026-07-01 15:39:43 -07:00
[pinned, rich, link, image, color, audio, video, file, code].forEach {
2026-06-30 01:12:19 -07:00
fixture.store.upsert($0)
drainMainQueue()
}
2026-07-01 15:39:43 -07:00
XCTAssertEqual(fixture.viewModel.visibleItems.count, 9)
XCTAssertEqual(ClipboardSortMode.allCases.map { fixture.viewModel.collectionCount(for: $0) }, [9, 9, 3, 1, 1, 1, 1, 1, 1, 1, 1])
XCTAssertEqual(fixture.view.debugCollectionCounts, [9, 9, 3, 1, 1, 1, 1, 1, 1, 1, 1])
2026-06-30 10:39:33 -07:00
XCTAssertEqual(fixture.view.debugCollectionCountLabelHiddenStates, Array(repeating: false, count: ClipboardSortMode.allCases.count))
2026-06-30 01:12:19 -07:00
}
func testCollectionRailShowsAssignedCollections() {
let fixture = makePanelFixture()
var link = makeItem(kind: .url, text: "https://example.com/read", store: fixture.store)
link.collectionName = "Useful Links"
var note = makeTextItem("Meeting note", store: fixture.store)
note.collectionName = "Important Notes"
var file = makeItem(kind: .file, text: "/tmp/client-brief.pdf", store: fixture.store)
file.collectionName = "Client Work"
fixture.store.upsert(link)
fixture.store.upsert(note)
fixture.store.upsert(file)
drainMainQueue()
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Useful Links", "Important Notes", "Client Work"])
XCTAssertEqual(fixture.view.debugCustomCollectionCounts, [1, 1, 1])
2026-06-30 10:39:33 -07:00
XCTAssertEqual(fixture.view.debugCustomCollectionCountLabelHiddenStates, [false, false, false])
2026-06-30 01:12:19 -07:00
fixture.viewModel.selectCollection(named: "Useful Links")
drainMainQueue()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["https://example.com/read"])
}
2026-06-30 03:02:29 -07:00
func testCardsCanDropOntoCollectionChipsToOrganize() {
let fixture = makePanelFixture()
var existing = makeTextItem("Existing client note", store: fixture.store)
existing.collectionName = "Client Work"
fixture.store.upsert(existing)
let dropped = makeTextItem("Drop this note", store: fixture.store)
fixture.store.upsert(dropped)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Client Work"])
XCTAssertEqual(fixture.view.debugCustomCollectionDropTargets, ["Client Work"])
XCTAssertEqual(fixture.viewModel.visibleItems.first?.id, dropped.id)
fixture.view.debugDropFirstCard(onCollectionNamed: "Client Work")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.statusMessage, "Added to Client Work")
XCTAssertEqual(fixture.view.debugCustomCollectionCounts, [2])
fixture.viewModel.selectCollection(named: "Client Work")
drainMainQueue()
XCTAssertEqual(Set(fixture.viewModel.visibleItems.map(\.payload)), ["Existing client note", "Drop this note"])
}
2026-06-30 01:12:19 -07:00
func testCollectionRailUsesScrollableDocumentForCrowdedCustomCollections() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
2026-06-30 01:12:19 -07:00
let names = [
"Client Work",
"Research Archive",
"Launch Planning",
"Design QA",
"Product References",
"Reading Stack",
"Invoices",
"Hiring Pipeline"
]
for name in names {
var item = makeTextItem("Collection item \(name)", store: fixture.store)
item.collectionName = name
fixture.store.upsert(item)
}
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(fixture.view.debugCollectionRailVisibleWidth, 0)
XCTAssertGreaterThan(
fixture.view.debugCollectionRailDocumentWidth,
fixture.view.debugCollectionRailVisibleWidth + 1
)
XCTAssertTrue(fixture.view.debugCustomCollectionTitles.contains("Client Work"))
XCTAssertTrue(fixture.view.debugCustomCollectionTitles.contains("Product References"))
XCTAssertEqual(fixture.view.debugCollectionRailVisibleRect.minX, 0, accuracy: 0.5)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCollectionRailOverflowFadeVisibility, [false, true])
fixture.view.debugScrollCollectionRailVertically(deltaY: -220)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(fixture.view.debugCollectionRailVisibleRect.minX, 0)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCollectionRailOverflowFadeVisibility, [true, true])
fixture.view.debugScrollCollectionRailVertically(deltaY: 10_000)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCollectionRailVisibleRect.minX, 0, accuracy: 0.5)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCollectionRailOverflowFadeVisibility, [false, true])
2026-06-30 01:12:19 -07:00
}
func testSelectionScrollsCardRailToKeepSelectedCardVisible() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
for index in 0..<8 {
fixture.store.upsert(makeTextItem("Scrollable clipboard item \(index)", store: fixture.store))
drainMainQueue()
}
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.selectFirstItem()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertLessThanOrEqual(fixture.view.debugCardRailVisibleRect.minX, 1)
fixture.viewModel.selectItem(at: fixture.viewModel.visibleItems.count - 1)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
let visibleRect = fixture.view.debugCardRailVisibleRect
let selectedFrame = fixture.view.debugSelectedCardFrameInDocument
XCTAssertGreaterThan(visibleRect.minX, 0)
XCTAssertLessThanOrEqual(selectedFrame.minX, visibleRect.maxX)
XCTAssertGreaterThanOrEqual(visibleRect.maxX + 1, selectedFrame.maxX)
fixture.viewModel.selectItem(at: 0)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertLessThanOrEqual(fixture.view.debugCardRailVisibleRect.minX, 1)
}
func testVerticalWheelPansHorizontalCardRailAndClamps() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
for index in 0..<8 {
fixture.store.upsert(makeTextItem("Wheel scroll item \(index)", store: fixture.store))
drainMainQueue()
}
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.selectItem(at: 0)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(
fixture.view.debugCardRailDocumentWidth,
fixture.view.debugCardRailVisibleRect.width + 1
)
XCTAssertEqual(fixture.view.debugCardRailVisibleRect.minX, 0, accuracy: 0.5)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, true])
fixture.view.debugScrollCardRailVertically(deltaY: -240)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(fixture.view.debugCardRailVisibleRect.minX, 0)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [true, true])
fixture.view.debugScrollCardRailVertically(deltaY: -10_000)
fixture.window.contentView?.layoutSubtreeIfNeeded()
let maxOffset = fixture.view.debugCardRailDocumentWidth - fixture.view.debugCardRailVisibleRect.width
XCTAssertEqual(fixture.view.debugCardRailVisibleRect.minX, maxOffset, accuracy: 1)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [true, false])
fixture.view.debugScrollCardRailVertically(deltaY: 10_000)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardRailVisibleRect.minX, 0, accuracy: 1)
2026-06-30 09:06:49 -07:00
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, true])
}
2026-06-30 01:12:19 -07:00
func testFilteredEmptyStateNamesCurrentCollection() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Only text exists", store: fixture.store))
drainMainQueue()
fixture.viewModel.sortMode = .images
drainMainQueue()
XCTAssertEqual(fixture.view.debugEmptyStateText?.title, "No images yet")
XCTAssertEqual(fixture.view.debugEmptyStateText?.detail, "Image clips are saved when the clipboard contains image data.")
}
func testPinnedEmptyStatePointsToPinAction() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Only text exists", store: fixture.store))
drainMainQueue()
fixture.viewModel.sortMode = .pinned
drainMainQueue()
XCTAssertEqual(fixture.view.debugEmptyStateText?.title, "No pinned clips")
XCTAssertEqual(fixture.view.debugEmptyStateText?.detail, "Use the Pin action on a card to keep important clips here.")
}
2026-06-30 01:12:19 -07:00
func testCardsExposeContextMenuActions() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Context menu text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
2026-07-01 15:20:13 -07:00
["Paste", "Copy", "Rename...", "Add to Stack", "Add Visible Clips to Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
2026-06-30 01:12:19 -07:00
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
2026-06-30 03:17:40 -07:00
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
2026-06-30 02:49:35 -07:00
XCTAssertEqual(
fixture.view.debugFirstCardCaptureRuleMenuTitles,
["Ignore Ghostty", "Ignore Text Items"]
)
2026-06-30 01:12:19 -07:00
}
2026-06-30 03:28:50 -07:00
func testFilteredCardsExposeShowInClipboardContextMenuAction() {
let fixture = makePanelFixture()
var release = makeTextItem("Release needle", store: fixture.store)
release.createdAt = Date(timeIntervalSince1970: 100)
release.lastUsedAt = release.createdAt
var meeting = makeTextItem("Meeting note", store: fixture.store)
meeting.createdAt = Date(timeIntervalSince1970: 200)
meeting.lastUsedAt = meeting.createdAt
fixture.store.upsert(release)
fixture.store.upsert(meeting)
drainMainQueue()
fixture.viewModel.searchText = "release"
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
2026-07-01 15:20:13 -07:00
["Paste", "Copy", "Show in Clipboard", "Rename...", "Add to Stack", "Add Visible Clips to Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
2026-06-30 03:28:50 -07:00
)
fixture.view.debugShowFirstCardInClipboard()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugSearchFieldText, "")
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Meeting note", "Release needle"])
XCTAssertEqual(fixture.viewModel.selectedItem?.payload, "Release needle")
}
2026-06-30 01:48:35 -07:00
func testPreviewableCardsExposeQuickLookContextMenuAction() {
let fixture = makePanelFixture()
fixture.store.upsert(makeItem(kind: .file, text: "/tmp/report.txt", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
2026-07-01 15:20:13 -07:00
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Rename...", "Add to Stack", "Add Visible Clips to Stack", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
2026-06-30 02:49:35 -07:00
)
XCTAssertEqual(
fixture.view.debugFirstCardCaptureRuleMenuTitles,
["Ignore Ghostty", "Ignore File Items"]
2026-06-30 01:48:35 -07:00
)
}
2026-06-30 02:11:50 -07:00
func testStackedCardsExposeStackManagementActions() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Stackable text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.toggleSelectedStackMembership()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
2026-07-01 15:20:13 -07:00
["Paste", "Copy", "Rename...", "Remove from Stack", "Add Visible Clips to Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
2026-06-30 02:11:50 -07:00
)
2026-06-30 10:34:37 -07:00
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Edit", "Preview", "Delete"])
XCTAssertEqual(fixture.view.debugStackCornerLabels, ["Remove from Stack"])
2026-06-30 02:11:50 -07:00
}
2026-06-30 10:30:07 -07:00
func testStackCornerButtonTogglesAndPersistsForQueuedCards() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Older stack item", store: fixture.store))
fixture.store.upsert(makeTextItem("Newest stack item", store: fixture.store))
drainMainQueue()
fixture.viewModel.selectItem(at: 0)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugStackCornerLabels, ["Add to Stack", "Add to Stack"])
XCTAssertEqual(fixture.view.debugStackCornerHiddenStates, [false, true])
2026-06-30 10:34:37 -07:00
XCTAssertFalse(fixture.view.debugFirstCardVisibleActionLabels.contains("Add to Stack"))
2026-06-30 10:30:07 -07:00
XCTAssertGreaterThan(fixture.view.debugFirstCardStackCornerFrame.maxX, 290)
fixture.view.debugPressFirstCardStackCornerButton()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugStatusText, "Added to Stack")
XCTAssertEqual(fixture.view.debugStackCornerLabels, ["Remove from Stack", "Add to Stack"])
XCTAssertEqual(fixture.view.debugStackCornerHiddenStates, [false, true])
XCTAssertTrue(fixture.view.debugStackChipIsVisible)
XCTAssertEqual(fixture.view.debugStackChipCount, 1)
fixture.viewModel.selectItem(at: 1)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugStackCornerHiddenStates, [false, false])
}
2026-06-30 02:19:03 -07:00
func testStackChipAppearsFiltersAndClearsWithStack() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("First stack chip item", store: fixture.store))
fixture.store.upsert(makeTextItem("Second stack chip item", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertFalse(fixture.view.debugStackChipIsVisible)
fixture.viewModel.selectItem(at: 1)
fixture.viewModel.toggleSelectedStackMembership()
fixture.viewModel.selectItem(at: 0)
fixture.viewModel.toggleSelectedStackMembership()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugStackChipIsVisible)
XCTAssertEqual(fixture.view.debugStackChipCount, 2)
XCTAssertFalse(fixture.view.debugStackChipIsSelected)
fixture.view.debugPressStackChip()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Stack")
XCTAssertTrue(fixture.view.debugStackChipIsSelected)
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["First stack chip item", "Second stack chip item"])
fixture.viewModel.clearStack()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertFalse(fixture.view.debugStackChipIsVisible)
XCTAssertEqual(fixture.view.debugStackChipCount, 0)
}
2026-07-01 15:20:13 -07:00
func testStackChipMenuAddsVisibleShelfToQueue() {
let fixture = makePanelFixture()
var older = makeTextItem("Older batch stack item", store: fixture.store)
older.createdAt = Date(timeIntervalSince1970: 100)
older.lastUsedAt = older.createdAt
var middle = makeTextItem("Middle batch stack item", store: fixture.store)
middle.createdAt = Date(timeIntervalSince1970: 200)
middle.lastUsedAt = middle.createdAt
var newest = makeTextItem("Newest batch stack item", store: fixture.store)
newest.createdAt = Date(timeIntervalSince1970: 300)
newest.lastUsedAt = newest.createdAt
fixture.store.upsert(older)
fixture.store.upsert(middle)
fixture.store.upsert(newest)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.selectItem(at: 0)
fixture.viewModel.toggleSelectedStackMembership()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugStackChipIsVisible)
XCTAssertEqual(fixture.view.debugStackChipCount, 1)
XCTAssertEqual(
fixture.view.debugStackChipMenuTitles,
["Add Visible Clips to Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack"]
)
fixture.view.debugAddVisibleClipsToStackFromStackChip()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugStackChipCount, 3)
XCTAssertEqual(fixture.view.debugStatusText, "Added 2 clips to Stack")
fixture.view.debugPressStackChip()
drainMainQueue()
XCTAssertEqual(
fixture.viewModel.visibleItems.map(\.payload),
["Newest batch stack item", "Middle batch stack item", "Older batch stack item"]
)
}
2026-06-30 01:12:19 -07:00
func testCollectionMenuOffersExistingCustomCollections() {
let fixture = makePanelFixture()
var existing = makeTextItem("Existing client note", store: fixture.store)
existing.collectionName = "Client Work"
fixture.store.upsert(existing)
fixture.store.upsert(makeTextItem("Unsorted card", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardCollectionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
)
2026-06-30 03:17:40 -07:00
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
)
2026-06-30 01:12:19 -07:00
}
func testBottomSafeInsetIsAppliedToPanelContent() {
let fixture = makePanelFixture()
fixture.view.setBottomSafeInset(108)
XCTAssertEqual(fixture.view.debugContentInsets.bottom, 108)
}
func testInternalLookingTextDoesNotBecomePrimaryCardTitle() {
let fixture = makePanelFixture()
let item = makeTextItem("clipbored-flow-test-\(UUID().uuidString)", store: fixture.store)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Text: Copied text"])
}
func testLinkCardsUseReadableTitleAndAddressPreview() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .url,
displayText: "Release notes",
payload: "https://www.example.com/releases/v1?utm_source=copy",
store: fixture.store
)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Link: Release notes"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Release notes|example.com/releases/v1|example.com"])
2026-07-01 14:59:52 -07:00
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["link-site-preview"])
}
func testPlainURLCardsDeriveReadableTitleFromPath() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .url,
displayText: "https://www.example.com/articles/weekly-design-review?utm_source=copy",
payload: "https://www.example.com/articles/weekly-design-review?utm_source=copy",
store: fixture.store
)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Link: Weekly Design Review"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Weekly Design Review|example.com/articles/weekly-design-review|example.com"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["link-site-preview"])
2026-06-30 01:12:19 -07:00
}
func testLinkCardsUseMediaPreviewWhenThumbnailExists() throws {
let fixture = makePanelFixture()
let id = UUID()
let paths = try XCTUnwrap(fixture.cacheService.cacheImage(sampleImage(), id: id))
let item = ClipboardItem(
id: id,
kind: .url,
displayText: "Lookbook",
payload: "https://example.com/lookbook",
payloadHash: fixture.store.hashString("https://example.com/lookbook"),
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: "Safari",
imagePath: paths.full,
thumbnailPath: paths.thumb
)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .links
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Link: Lookbook"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Lookbook|example.com/lookbook|example.com"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["link-media-preview"])
}
func testRichTextCardsUseDisplayTextInsteadOfManagedPayloadPath() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .richText,
displayText: "Styled note",
payload: "/tmp/clipbored-managed-rich-text.rtf",
store: fixture.store
)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Rich Text: Styled note"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Styled note|Styled note|11 characters"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["rich-text-preview"])
}
func testFileCardsUseFilenameLocationAndType() {
let fixture = makePanelFixture()
let fileURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Documents")
.appendingPathComponent("Project Plan.pdf")
let item = makeItem(kind: .file, displayText: "File", payload: fileURL.path, store: fixture.store)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["File: Project Plan.pdf"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Project Plan.pdf|~/Documents|PDF"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["file-preview"])
}
2026-06-30 04:10:12 -07:00
func testRenamedClipsUseCustomTitleInCardsAndSearch() {
let fixture = makePanelFixture()
let fileURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Documents")
.appendingPathComponent("Project Plan.pdf")
let item = makeItem(kind: .file, displayText: "File", payload: fileURL.path, store: fixture.store)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFirstCardMenuTitles.contains("Rename..."))
fixture.view.debugRenameFirstCard(to: " Client Launch Brief ")
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["File: Client Launch Brief"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Client Launch Brief|~/Documents|PDF"])
XCTAssertEqual(fixture.view.debugStatusText, "Renamed clip")
XCTAssertEqual(fixture.view.debugStatusTone, "action")
fixture.viewModel.searchText = "launch"
drainMainQueue()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.id), [item.id])
}
2026-06-30 01:12:19 -07:00
func testMultipleFileCardsUseCountAndSharedLocation() throws {
let fixture = makePanelFixture()
let directory = makeTempDirectory()
let firstURL = directory.appendingPathComponent("Brief.pdf")
let secondURL = directory.appendingPathComponent("Invoice.csv")
try Data("brief".utf8).write(to: firstURL)
try Data("invoice".utf8).write(to: secondURL)
let item = makeItem(
kind: .file,
displayText: "2 files",
payload: FilePayload.payload(from: [firstURL, secondURL]),
store: fixture.store
)
fixture.store.upsert(item)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["File: 2 files"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["2 files|\(directory.path)|2 files"])
2026-07-01 14:33:04 -07:00
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["multi-file-preview"])
2026-06-30 01:12:19 -07:00
}
func testExistingFileCardsUseFullBleedMediaPreviewLayout() throws {
let fixture = makePanelFixture()
let imageURL = makeTempDirectory().appendingPathComponent("Campaign Reference.png")
let imageData = try XCTUnwrap(sampleImage().pngData())
try imageData.write(to: imageURL)
let item = makeItem(kind: .file, displayText: "File", payload: imageURL.path, store: fixture.store)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .files
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["File: Campaign Reference.png"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["file-media-preview"])
}
func testPdfAndImageCardsUseSpecificPreviewText() {
let fixture = makePanelFixture()
let pdfURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Downloads")
.appendingPathComponent("Reference Guide.pdf")
let pdf = makeItem(
kind: .pdf,
displayText: "PDF",
payload: pdfURL.path,
store: fixture.store,
ocrText: "Quarterly metrics\nSecond page"
)
fixture.store.upsert(pdf)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["PDF: Reference Guide.pdf"])
XCTAssertEqual(
fixture.view.debugCardPreviewSummaries,
["Reference Guide.pdf|Quarterly metrics Second page|PDF"]
)
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["file-preview"])
let image = makeItem(
kind: .image,
displayText: "Image",
payload: "",
store: fixture.store,
ocrText: "Receipt total $42"
)
fixture.store.upsert(image)
fixture.viewModel.sortMode = .images
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Image: Receipt total $42"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Receipt total $42|Receipt total $42|OCR text"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["text-fallback-preview"])
}
func testImageCardsUseMediaPreviewWhenThumbnailExists() {
let fixture = makePanelFixture()
let item = makeCachedImageItem(store: fixture.store, cacheService: fixture.cacheService)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .images
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Image: Campaign portrait"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["media-preview"])
}
func testAudioCardsUseSpecificPreviewText() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .audio,
displayText: "Audio (14 KB)",
payload: "/tmp/clipbored-audio.sound",
store: fixture.store
)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .audio
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Audio: Audio (14 KB)"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Audio (14 KB)|Sound clip|Audio"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["audio-preview"])
}
2026-07-01 15:39:43 -07:00
func testVideoCardsUseFilmPreview() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .video,
displayText: "Video (24 KB)",
payload: "/tmp/clipbored-video.mp4",
store: fixture.store
)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .videos
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Video: Video (24 KB)"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["Video (24 KB)|Video clip|MP4"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["video-preview"])
}
2026-07-01 14:52:54 -07:00
func testColorCardsUseSwatchPreview() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .color,
displayText: "#0A84FF",
payload: "#0A84FF",
store: fixture.store
)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .colors
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Color: #0A84FF"])
XCTAssertEqual(fixture.view.debugCardPreviewSummaries, ["#0A84FF|RGB 10 132 255|Color"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["color-preview"])
}
2026-07-01 15:12:49 -07:00
func testCodeCardsUseMonospaceSnippetPreview() {
let fixture = makePanelFixture()
let item = makeItem(
kind: .code,
displayText: "Swift Snippet",
payload: "func greet(name: String) -> String {\n return \"Hi \\(name)\"\n}",
store: fixture.store
)
fixture.store.upsert(item)
fixture.viewModel.sortMode = .code
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardAccessibilityLabels, ["Code: Swift Snippet"])
XCTAssertEqual(
fixture.view.debugCardPreviewSummaries,
["Swift Snippet|func greet(name: String) -> String { return \"Hi \\(name)\" }|Swift"]
)
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["code-preview"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Edit", "Preview", "Delete"])
}
2026-06-30 01:12:19 -07:00
private func makePanelWithPanelView() -> (NSWindow, ClipboardPanelView) {
let fixture = makePanelFixture()
return (fixture.window, fixture.view)
}
private func makePanelFixture() -> PanelFixture {
let settings = makeSettings()
let cacheService = ClipboardCacheService()
let store = makeStore(settings: settings, cacheService: cacheService)
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
let previewProbe = PreviewProbe()
2026-06-30 01:12:19 -07:00
let view = ClipboardPanelView(
viewModel: viewModel,
onClose: {},
onSettings: {},
onPreview: { previewProbe.requestCount += 1 }
2026-06-30 01:12:19 -07:00
)
let window = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 520),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.contentView = view
window.makeKeyAndOrderFront(nil)
return PanelFixture(
window: window,
view: view,
viewModel: viewModel,
settings: settings,
store: store,
cacheService: cacheService,
previewProbe: previewProbe
2026-06-30 01:12:19 -07:00
)
}
private func makeSettings() -> SettingsModel {
let settings = SettingsModel(defaults: UserDefaults(suiteName: "com.clipbored.viewtest.\(UUID().uuidString)")!)
settings.maxHistoryItems = 10
settings.pruneDuplicates = false
return settings
}
private func makeStore(settings: SettingsModel, cacheService: ClipboardCacheService) -> ClipboardStore {
ClipboardStore(settings: settings, cacheService: cacheService, baseURL: makeTempDirectory())
}
private func makeTempDirectory() -> URL {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("clipbored-viewtest")
.appendingPathComponent(UUID().uuidString)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
tempURLs.append(directory)
return directory
}
private func makeTextItem(_ text: String, store: ClipboardStore) -> ClipboardItem {
makeItem(kind: .text, text: text, store: store)
}
private func makeItem(kind: ClipboardItemKind, text: String, store: ClipboardStore) -> ClipboardItem {
makeItem(kind: kind, displayText: text, payload: text, store: store)
}
private func makeItem(
kind: ClipboardItemKind,
displayText: String,
payload: String,
store: ClipboardStore,
ocrText: String? = nil
) -> ClipboardItem {
ClipboardItem(
id: UUID(),
kind: kind,
displayText: displayText,
payload: payload,
payloadHash: store.hashString(payload),
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: "Ghostty",
imagePath: nil,
thumbnailPath: nil,
ocrText: ocrText
)
}
private func makeCachedImageItem(store: ClipboardStore, cacheService: ClipboardCacheService) -> ClipboardItem {
let id = UUID()
let paths = cacheService.cacheImage(sampleImage(), id: id)
return ClipboardItem(
id: id,
kind: .image,
displayText: "Campaign portrait",
payload: paths?.full ?? "",
payloadHash: store.hashString("campaign-portrait"),
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: "Photos",
imagePath: paths?.full,
thumbnailPath: paths?.thumb,
ocrText: nil
)
}
private func sampleImage() -> NSImage {
let image = NSImage(size: NSSize(width: 180, height: 140))
image.lockFocus()
NSColor(calibratedRed: 1.0, green: 0.76, blue: 0.20, alpha: 1).setFill()
NSRect(x: 0, y: 0, width: 180, height: 140).fill()
NSColor(calibratedRed: 0.92, green: 0.20, blue: 0.26, alpha: 1).setFill()
NSBezierPath(ovalIn: NSRect(x: 34, y: 24, width: 82, height: 82)).fill()
NSColor(calibratedRed: 0.05, green: 0.42, blue: 0.86, alpha: 1).setFill()
NSBezierPath(roundedRect: NSRect(x: 92, y: 45, width: 58, height: 48), xRadius: 12, yRadius: 12).fill()
image.unlockFocus()
return image
}
private func drainMainQueue() {
for _ in 0..<20 {
RunLoop.main.run(until: Date().addingTimeInterval(0.01))
}
}
}