1558 lines
63 KiB
Swift
1558 lines
63 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", "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", "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 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() -> 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()
|
|
|
|
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))
|
|
}
|
|
}
|
|
}
|