Files
clipbored/tests/clipboredtests/ClipboardPanelViewTests.swift
Akshay Kolli c7316105c7
Some checks are pending
CI / Test And Build (push) Waiting to run
WIP: add stack text batch actions
2026-07-01 15:58:30 -07:00

1591 lines
64 KiB
Swift

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
}
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)
}
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")
}
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"])
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, false])
}
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"])
}
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
)
}
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"])
}
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"))
}
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")
}
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")
}
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,
["Clipboard", "Frequent", "Text", "Links", "Images", "Colors", "Audio", "Videos", "Files", "Pinned", "Code"]
)
XCTAssertEqual(
fixture.view.debugCollectionLeadingSymbols,
["doc.on.clipboard", "chart.bar.fill", "text.alignleft", "link", "photo", "paintpalette", "music.note", "film", "doc.fill", "pin.fill", "chevron.left.forwardslash.chevron.right"]
)
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))
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"])
}
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"])
}
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"])
}
func testCollectionRailAddButtonCreatesEmptyCollection() {
let fixture = makePanelFixture()
XCTAssertTrue(fixture.view.debugCollectionRailContainsAddButton)
XCTAssertTrue(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.view.debugSetCollectionNameProvider { " Research Stack " }
fixture.view.debugPressAddCollectionButton()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.statusMessage, "Created Research Stack")
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Research Stack"])
XCTAssertEqual(fixture.view.debugCustomCollectionCounts, [0])
XCTAssertEqual(fixture.view.debugCustomCollectionCountLabelHiddenStates, [true])
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.")
}
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)
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Collect this note"])
XCTAssertEqual(fixture.view.debugFirstCardHeaderTitle, "Research Stack")
XCTAssertEqual(fixture.view.debugFirstCardHeaderSubtitle, "Text - Just now")
XCTAssertEqual(fixture.view.debugFirstCardHeaderColorHex, "#0A9EB8")
XCTAssertEqual(fixture.view.debugCustomCollectionColorHexes["Research Stack"], "#0A9EB8")
XCTAssertEqual(fixture.view.debugFirstCardFooterDetailText, "17 characters")
}
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")
}
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), [])
}
func testSelectedCardActionsRespectSelectedKind() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Edit", "Preview", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertFalse(fixture.view.debugFirstCardHeaderBadgeIsHidden)
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)
fixture.store.upsert(makeItem(kind: .file, text: "/tmp/report.txt", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.selectFirstItem()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Paste Plain Text", "Collect", "Preview", "Open", "Reveal", "More"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertFalse(fixture.view.debugFirstCardHeaderBadgeIsHidden)
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)
}
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")
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)
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,
["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")
XCTAssertEqual(fixture.view.debugCardAccessibilityHelps[1], "Press Return to paste. Press Space for Quick Look.")
fixture.view.debugPressFocusedResponderWithReturn()
drainMainQueue()
XCTAssertEqual(fixture.viewModel.statusMessage, "Copied")
}
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, "")
}
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"])
}
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")
}
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])
}
func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable() {
let fixture = makePanelFixture()
var item = makeItem(kind: .url, text: "https://example.com", store: fixture.store)
item.sourceApp = nil
fixture.store.upsert(item)
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"])
}
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)
let color = makeItem(kind: .color, displayText: "#0A84FF", payload: "#0A84FF", store: fixture.store)
let audio = makeItem(kind: .audio, text: "audio payload", store: fixture.store)
let video = makeItem(kind: .video, text: "/tmp/movie.mp4", store: fixture.store)
let file = makeItem(kind: .file, text: "/tmp/report.pdf", store: fixture.store)
let code = makeItem(
kind: .code,
displayText: "Swift Snippet",
payload: "func greet(name: String) -> String {\n return \"Hi \\(name)\"\n}",
store: fixture.store
)
[pinned, rich, link, image, color, audio, video, file, code].forEach {
fixture.store.upsert($0)
drainMainQueue()
}
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])
XCTAssertEqual(fixture.view.debugCollectionCountLabelHiddenStates, Array(repeating: false, count: ClipboardSortMode.allCases.count))
}
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])
XCTAssertEqual(fixture.view.debugCustomCollectionCountLabelHiddenStates, [false, false, false])
fixture.viewModel.selectCollection(named: "Useful Links")
drainMainQueue()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["https://example.com/read"])
}
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"])
}
func testCollectionRailUsesScrollableDocumentForCrowdedCustomCollections() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
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)
XCTAssertEqual(fixture.view.debugCollectionRailOverflowFadeVisibility, [false, true])
fixture.view.debugScrollCollectionRailVertically(deltaY: -220)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(fixture.view.debugCollectionRailVisibleRect.minX, 0)
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)
XCTAssertEqual(fixture.view.debugCollectionRailOverflowFadeVisibility, [false, true])
}
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)
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, true])
fixture.view.debugScrollCardRailVertically(deltaY: -240)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertGreaterThan(fixture.view.debugCardRailVisibleRect.minX, 0)
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)
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [true, false])
fixture.view.debugScrollCardRailVertically(deltaY: 10_000)
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardRailVisibleRect.minX, 0, accuracy: 1)
XCTAssertEqual(fixture.view.debugCardRailOverflowFadeVisibility, [false, true])
}
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.")
}
func testCardsExposeContextMenuActions() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Context menu text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
["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"]
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCaptureRuleMenuTitles,
["Ignore Ghostty", "Ignore Text Items"]
)
}
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,
["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"]
)
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")
}
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,
["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"]
)
XCTAssertEqual(
fixture.view.debugFirstCardCaptureRuleMenuTitles,
["Ignore Ghostty", "Ignore File Items"]
)
}
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,
["Paste", "Copy", "Rename...", "Remove from Stack", "Add Visible Clips to Stack", "Paste Stack Next", "Copy Stack Next", "Paste Stack as Text", "Copy Stack as Text", "Clear Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Edit", "Preview", "Delete"])
XCTAssertEqual(fixture.view.debugStackCornerLabels, ["Remove from Stack"])
}
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])
XCTAssertFalse(fixture.view.debugFirstCardVisibleActionLabels.contains("Add to Stack"))
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])
}
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)
}
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", "Paste Stack as Text", "Copy Stack as Text", "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"]
)
}
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..."]
)
XCTAssertEqual(
fixture.view.debugFirstCardCollectActionMenuTitles,
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "Client Work", "-", "New Collection..."]
)
}
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"])
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"])
}
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"])
}
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])
}
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"])
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["multi-file-preview"])
}
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"])
}
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"])
}
func testVideoCardsUseMediaPreviewWhenThumbnailExists() throws {
let cacheService = ClipboardCacheService(
baseURL: makeTempDirectory(),
encryptionService: ClipboardEncryptionService(keyProvider: { nil }),
videoThumbnailProvider: { _ in self.sampleImage() }
)
let fixture = makePanelFixture(cacheService: cacheService)
let id = UUID()
let path = try XCTUnwrap(cacheService.cacheVideo(Data([0, 1, 2, 3]), id: id, fileExtension: "mp4"))
let item = ClipboardItem(
id: id,
kind: .video,
displayText: "Video (24 KB)",
payload: path,
payloadHash: fixture.store.hashString("video-thumbnail"),
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: "QuickTime Player",
imagePath: nil,
thumbnailPath: nil
)
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-media-preview"])
}
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"])
}
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"])
}
private func makePanelWithPanelView() -> (NSWindow, ClipboardPanelView) {
let fixture = makePanelFixture()
return (fixture.window, fixture.view)
}
private func makePanelFixture(cacheService: ClipboardCacheService? = nil) -> PanelFixture {
let settings = makeSettings()
let cacheService = cacheService ?? ClipboardCacheService()
let store = makeStore(settings: settings, cacheService: cacheService)
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
let previewProbe = PreviewProbe()
let view = ClipboardPanelView(
viewModel: viewModel,
onClose: {},
onSettings: {},
onPreview: { previewProbe.requestCount += 1 }
)
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
)
}
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))
}
}
}