2026-06-30 01:12:19 -07:00
import AppKit
import XCTest
@ testable import ClipBored
final class ClipboardPanelViewTests : XCTestCase {
private var tempURLs : [ URL ] = [ ]
private struct PanelFixture {
let window : NSWindow
let view : ClipboardPanelView
let viewModel : ClipboardPanelViewModel
let settings : SettingsModel
let store : ClipboardStore
let cacheService : ClipboardCacheService
2026-06-30 08:56:45 -07:00
let previewProbe : PreviewProbe
}
private final class PreviewProbe {
var requestCount = 0
2026-06-30 01:12:19 -07:00
}
override func tearDown ( ) {
tempURLs . forEach { try ? FileManager . default . removeItem ( at : $0 ) }
tempURLs . removeAll ( )
super . tearDown ( )
}
func testSearchFieldEditingWhenSearchFieldIsFirstResponder ( ) {
let ( window , view ) = makePanelWithPanelView ( )
window . makeFirstResponder ( view )
XCTAssertFalse ( view . isSearchFieldEditing )
view . focusSearchField ( )
XCTAssertTrue ( view . isSearchFieldEditing )
}
func testSearchFieldEditingWhenFieldEditorIsFirstResponder ( ) {
let ( window , view ) = makePanelWithPanelView ( )
view . focusSearchField ( )
guard let editor = window . fieldEditor ( false , for : nil ) else {
return XCTFail ( " Expected a search field editor " )
}
window . makeFirstResponder ( editor )
XCTAssertTrue ( view . isSearchFieldEditing )
}
2026-07-09 19:32:57 -04:00
func testSearchUsesOneLeadingAnchoredLiquidGlassControlInBothStates ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchGlassMaterial , . popover )
XCTAssertEqual ( fixture . view . debugSearchGlassBlendingMode , . withinWindow )
XCTAssertEqual ( fixture . view . debugSearchGlassCornerRadius , 15 , accuracy : 0.5 )
XCTAssertGreaterThan ( fixture . view . debugSearchGlassBorderAlpha , 0.35 )
XCTAssertGreaterThan ( fixture . view . debugSearchGlassTintAlpha , 0.05 )
XCTAssertTrue ( fixture . view . debugSearchFieldUsesTransparentChrome )
XCTAssertTrue (
fixture . view . debugSearchIconIsPinnedToLeadingEdge ,
" Icon leading offset: \( fixture . view . debugSearchIconLeadingOffset ) "
)
XCTAssertTrue (
fixture . view . debugSearchControlIsLeadingAlignedInToolbar ,
" Toolbar leading offset: \( fixture . view . debugSearchControlToolbarLeadingOffset ) "
)
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
fixture . view . focusSearchField ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchGlassMaterial , . popover )
XCTAssertEqual ( fixture . view . debugSearchGlassCornerRadius , 15 , accuracy : 0.5 )
XCTAssertTrue ( fixture . view . debugSearchFieldUsesTransparentChrome )
XCTAssertTrue ( fixture . view . debugSearchFieldIsCenteredInGlass )
XCTAssertTrue (
fixture . view . debugSearchControlIsLeadingAlignedInToolbar ,
" Toolbar leading offset: \( fixture . view . debugSearchControlToolbarLeadingOffset ) "
)
XCTAssertTrue ( fixture . view . debugSearchIconIsPinnedToLeadingEdge )
XCTAssertGreaterThanOrEqual ( fixture . view . debugSearchTextLeadingGap , 4 )
XCTAssertGreaterThanOrEqual ( fixture . view . debugSearchEditorLeadingGap , 4 )
XCTAssertEqual ( fixture . view . debugSearchControlTrailingActionGap , 12 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 238 , accuracy : 0.5 )
}
2026-07-08 01:13:02 -04:00
func testSearchFieldExpandsWhileFocusedEvenWhenEmptyAndCollapsesWhenIdle ( ) {
2026-07-01 14:39:36 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Search focus polish " , store : fixture . store ) )
2026-07-01 14:39:36 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
2026-07-01 14:39:36 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . focusSearchField ( )
2026-07-01 14:39:36 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchControlHeight , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchIconButtonHeight , 30 , accuracy : 0.5 )
2026-07-01 14:39:36 -07:00
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " Search clips " )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchControlHeight , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchIconButtonHeight , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-06-30 01:12:19 -07:00
}
2026-07-08 01:13:02 -04:00
func testSearchFieldCollapsesWhenClearedWhileIdle ( ) {
2026-06-30 10:44:11 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Idle search collapse " , store : fixture . store ) )
2026-06-30 10:44:11 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 10:44:11 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugSetSearchFieldText ( " idle " )
2026-06-30 10:44:11 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " Search clips " )
2026-06-30 10:44:11 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugSetSearchFieldText ( " " )
2026-06-30 03:11:40 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchIconButtonCornerRadius , 15 , accuracy : 0.5 )
2026-06-30 03:11:40 -07:00
}
2026-07-08 01:13:02 -04:00
func testWhitespaceOnlySearchTextKeepsFieldExpandedUntilCleared ( ) {
2026-06-30 02:38:48 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Whitespace search presentation " , store : fixture . store ) )
drainMainQueue ( )
2026-06-30 02:38:48 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 02:38:48 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
fixture . view . debugSetSearchFieldText ( " " )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " Search clips " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Whitespace search presentation " ] )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 10:14:25 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . clearSearchForKeyboardCancel ( ) )
2026-06-30 10:14:25 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugSearchFieldIsVisible )
2026-07-09 19:32:57 -04:00
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-06-30 10:14:25 -07:00
}
2026-07-08 01:13:02 -04:00
func testPrepareForShowClearsSearchAndLeavesFieldCollapsedUntilTyping ( ) {
2026-06-30 10:14:25 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Quantum launch note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Alpha planning note " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 10:14:25 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugSetSearchFieldText ( " alpha " )
2026-06-30 10:14:25 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Alpha planning note " ] )
2026-06-30 10:14:25 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . prepareForShow ( )
2026-06-30 01:56:40 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:56:40 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertFalse ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Alpha planning note " , " Quantum launch note " ] )
XCTAssertTrue ( fixture . window . makeFirstResponder ( fixture . view ) )
fixture . view . debugTypeFocusedResponder ( " q " , keyCode : 12 )
2026-06-30 01:56:40 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:56:40 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " q " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugSearchFieldIsVisible )
2026-07-09 19:32:57 -04:00
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Quantum launch note " ] )
2026-06-30 01:56:40 -07:00
}
2026-07-08 01:13:02 -04:00
func testCommandFStyleSearchActionKeepsFocusInSearchField ( ) {
let ( window , view ) = makePanelWithPanelView ( )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
window . makeFirstResponder ( view )
2026-07-09 19:32:57 -04:00
view . focusSearch ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( view . isSearchFieldEditing )
2026-06-30 01:12:19 -07:00
2026-07-09 19:32:57 -04:00
view . focusSearch ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( view . isSearchFieldEditing )
2026-06-30 01:12:19 -07:00
}
2026-07-08 01:13:02 -04:00
func testPlainTextEditorsOptIntoWritingToolsWhenRuntimeSupportsIt ( ) {
let textView = ClipboardPanelView . makePlainTextEditor ( initialText : " Rewrite this launch note " )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( textView . string , " Rewrite this launch note " )
XCTAssertFalse ( textView . isRichText )
XCTAssertFalse ( textView . importsGraphics )
XCTAssertTrue ( textView . allowsUndo )
XCTAssertTrue ( textView . isAutomaticSpellingCorrectionEnabled )
XCTAssertTrue ( textView . isAutomaticTextReplacementEnabled )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
if textView . responds ( to : NSSelectorFromString ( " setWritingToolsBehavior: " ) ) {
XCTAssertEqual ( textView . value ( forKey : " writingToolsBehavior " ) as ? Int , 1 )
}
if textView . responds ( to : NSSelectorFromString ( " setAllowedWritingToolsResultOptions: " ) ) {
XCTAssertEqual ( textView . value ( forKey : " allowedWritingToolsResultOptions " ) as ? Int , 1 )
}
2026-06-30 01:12:19 -07:00
}
2026-07-09 19:32:57 -04:00
func testShelfChromeKeepsToolbarAlignedAboveSideCategoryRail ( ) {
2026-06-30 01:12:19 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Compact shelf chrome " , store : fixture . store ) )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugShelfChromeRowCount , 1 )
XCTAssertFalse ( fixture . view . debugShelfChromeContainsSearchAndCollections )
XCTAssertTrue ( fixture . view . debugShelfChromeContainsSearchAndActions )
XCTAssertTrue ( fixture . view . debugCollectionRailIsBesideCardRail )
XCTAssertTrue ( fixture . view . debugSearchFieldSharesAnimatedContainerWithIcon )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchAnimationDuration , 0.20 , accuracy : 0.01 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertFalse ( fixture . view . debugSearchFieldIsVisible )
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
XCTAssertEqual ( fixture . view . debugCollectionStackOrientation , . vertical )
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleWidth , 36 , accuracy : 0.5 )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCollectionRailHeight , 200 )
XCTAssertLessThanOrEqual (
fixture . view . debugCollectionRailFrameInPanel . maxX + 8 ,
fixture . view . debugSelectedCardFrameInPanel . minX + 0.5
2026-07-08 01:13:02 -04:00
)
XCTAssertEqual ( fixture . view . debugUtilityToolbarGroupBackgroundAlpha , 0 , accuracy : 0.01 )
XCTAssertEqual ( fixture . view . debugUtilityToolbarGroupCornerRadius , 14 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugToolbarButtonBackgroundAlphas . count , 2 )
fixture . view . debugToolbarButtonBackgroundAlphas . forEach { alpha in
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( alpha , 0.06 , accuracy : 0.01 )
2026-07-08 01:13:02 -04:00
}
XCTAssertEqual ( fixture . view . debugToolbarButtonBorderWidths , [ 0.5 , 0.5 ] )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugSetSearchFieldText ( " type:text " )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " Search clips " )
XCTAssertTrue ( fixture . view . debugSearchFieldIsVisible )
2026-07-09 19:32:57 -04:00
XCTAssertTrue ( fixture . view . debugSearchIconButtonIsVisible )
2026-07-08 01:13:02 -04:00
}
2026-06-30 01:12:19 -07:00
2026-07-09 19:32:57 -04:00
func testCollectionRailStaysBesideCardsAcrossSelection ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Centered text category " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/centered " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " centered-image " , store : fixture . store ) )
drainMainQueue ( )
2026-06-30 01:12:19 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
assertCollectionRailIsSideIconRail ( in : fixture )
fixture . view . debugMouseDownCollectionChip ( . text )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
assertCollectionRailIsSideIconRail ( in : fixture )
2026-06-30 01:12:19 -07:00
}
2026-07-09 19:32:57 -04:00
func testHoveringCategoryOnlyChangesChromeAndDoesNotMutateFiltering ( ) {
2026-06-30 01:12:19 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Plain text clip " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/preview " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " preview-image " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:12:19 -07:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . kind ) ) , Set ( [ . text , . url , . image ] ) )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugHoverCollectionChip ( . links )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:12:19 -07:00
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . kind ) ) , Set ( [ . text , . url , . image ] ) )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugUnhoverCollectionChip ( . links )
fixture . view . debugHoverCollectionChip ( . text )
2026-06-30 08:50:21 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . kind ) ) , Set ( [ . text , . url , . image ] ) )
2026-06-30 08:50:21 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugUnhoverCollectionChip ( . text )
2026-06-30 08:50:21 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 08:50:21 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . kind ) ) , Set ( [ . text , . url , . image ] ) )
2026-06-30 08:50:21 -07:00
}
2026-07-09 19:32:57 -04:00
func testClickingHoveredCategorySelectsItWithoutHoverSideEffects ( ) {
2026-06-30 09:26:28 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Commit text clip " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/commit " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " commit-image " , store : fixture . store ) )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 09:26:28 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugHoverCollectionChip ( . images )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . kind ) ) , Set ( [ . text , . url , . image ] ) )
2026-06-30 09:26:28 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugMouseDownCollectionChip ( . images )
fixture . view . debugUnhoverCollectionChip ( . images )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 09:26:28 -07:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Images " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . kind ) , [ . image ] )
}
2026-06-30 09:26:28 -07:00
2026-07-08 01:13:02 -04:00
func testSideShelfKeepsCardRailTuckedUnderToolbar ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 1280 , height : 640 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Tall shelf top spacing " , store : fixture . store ) )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 09:26:28 -07:00
2026-07-09 19:32:57 -04:00
XCTAssertLessThanOrEqual ( fixture . view . debugCardRailTopGap , 120 )
2026-07-08 01:13:02 -04:00
}
func testSideShelfUsesRowsAndExpandsSelection ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Side first compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Side second compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Side third compact row " , store : fixture . store ) )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 09:26:28 -07:00
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugPanelLayout , " Side Shelf " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugItemsStackOrientation , . vertical )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-row " ] )
XCTAssertGreaterThan ( fixture . view . debugCardSizes [ 0 ] . width , fixture . view . debugCardSizes [ 0 ] . height )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThanOrEqual ( fixture . view . debugCardSizes [ 0 ] . width , 240 )
2026-07-08 01:13:02 -04:00
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 0 ] . height , 72 )
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 2 ] . height , 72 )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 1 ) )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 09:26:28 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-focus " , " vertical-row " ] )
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 0 ] . height , 72 )
XCTAssertGreaterThanOrEqual ( fixture . view . debugCardSizes [ 1 ] . height , 200 )
}
func testVerticalShelfUsesRowsAndExpandsSelection ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 380 , height : 760 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Vertical first compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Vertical second compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Vertical third compact row " , store : fixture . store ) )
2026-06-30 09:26:28 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-row " ] )
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 0 ] . height , 72 )
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 1 ] . height , 72 )
XCTAssertGreaterThan ( fixture . view . debugCardSizes [ 1 ] . width , fixture . view . debugCardSizes [ 1 ] . height )
XCTAssertEqual ( fixture . view . debugCardSizes [ 2 ] . height , 56 , accuracy : 0.5 )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 1 ) )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-focus " , " vertical-row " ] )
XCTAssertLessThanOrEqual ( fixture . view . debugCardSizes [ 0 ] . height , 72 )
XCTAssertEqual ( fixture . view . debugCardSizes [ 1 ] . height , 220 , accuracy : 0.5 )
2026-06-30 09:26:28 -07:00
}
2026-07-08 01:13:02 -04:00
func testSideShelfAnimatesCardExpansionWhenSelectionMoves ( ) {
2026-06-30 09:40:04 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Animation first compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Animation second compact row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Animation third compact row " , store : fixture . store ) )
2026-06-30 09:40:04 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-row " ] )
let layoutChangeCount = fixture . view . debugAnimatedCardLayoutChangeCount
let presentationChangeCounts = fixture . view . debugCardAnimatedPresentationChangeCounts
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardExpansionAnimationDuration , 0.22 , accuracy : 0.01 )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 1 ) )
2026-06-30 09:40:04 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-focus " , " vertical-row " ] )
XCTAssertGreaterThan ( fixture . view . debugAnimatedCardLayoutChangeCount , layoutChangeCount )
XCTAssertGreaterThan ( fixture . view . debugCardAnimatedPresentationChangeCounts [ 1 ] , presentationChangeCounts [ 1 ] )
XCTAssertGreaterThan ( fixture . view . debugCardAnimatedPresentationChangeCounts [ 2 ] , presentationChangeCounts [ 2 ] )
2026-06-30 09:40:04 -07:00
}
2026-07-08 01:13:02 -04:00
func testHoverExpansionKeepsHeaderStableAndRollsDetailBelowIt ( ) {
2026-06-30 09:44:45 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Stable header first row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Stable header second row " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Stable header third row " , store : fixture . store ) )
2026-06-30 09:44:45 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
let headerBefore = fixture . view . debugCardHeaderFramesInPanel [ 1 ]
let detailBefore = fixture . view . debugCardExpandedDetailFramesInPanel [ 1 ]
let cornerRadiusBefore = fixture . view . debugCardContentCornerRadii [ 1 ]
let followingSlotClosedY = fixture . view . debugCardSlotPresentationFramesInPanel [ 2 ] . minY
let layoutChangeCount = fixture . view . debugAnimatedCardLayoutChangeCount
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-row " ] )
XCTAssertEqual ( detailBefore . height , 0 , accuracy : 0.5 )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCardContentBackgroundAlphas [ 1 ] , 0.5 )
2026-07-08 01:13:02 -04:00
fixture . view . debugHoverCard ( at : 1 )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( 0.06 ) )
let detailDuringAnimation = fixture . view . debugCardExpandedDetailPresentationHeights [ 1 ]
XCTAssertGreaterThan ( detailDuringAnimation , detailBefore . height + 4 )
XCTAssertLessThan ( detailDuringAnimation , 220 )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
let headerAfter = fixture . view . debugCardHeaderFramesInPanel [ 1 ]
let detailAfter = fixture . view . debugCardExpandedDetailFramesInPanel [ 1 ]
let cornerRadiusAfter = fixture . view . debugCardContentCornerRadii [ 1 ]
let followingSlotExpandedY = fixture . view . debugCardSlotPresentationFramesInPanel [ 2 ] . minY
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-focus " , " vertical-row " ] )
XCTAssertGreaterThan ( fixture . view . debugAnimatedCardLayoutChangeCount , layoutChangeCount )
XCTAssertEqual ( headerAfter . minX , headerBefore . minX , accuracy : 0.5 )
XCTAssertEqual ( headerAfter . minY , headerBefore . minY , accuracy : 0.5 )
XCTAssertEqual ( headerAfter . width , headerBefore . width , accuracy : 0.5 )
XCTAssertEqual ( headerAfter . height , headerBefore . height , accuracy : 0.5 )
XCTAssertEqual ( cornerRadiusAfter , cornerRadiusBefore , accuracy : 0.5 )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCardContentBackgroundAlphas [ 1 ] , 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertGreaterThan ( detailAfter . height , detailBefore . height + 120 )
XCTAssertTrue (
abs ( detailAfter . maxY - headerAfter . minY ) <= 1
|| abs ( detailAfter . minY - headerAfter . maxY ) <= 1
)
XCTAssertGreaterThan ( abs ( followingSlotExpandedY - followingSlotClosedY ) , 100 )
let layoutChangeCountAfterHover = fixture . view . debugAnimatedCardLayoutChangeCount
let detailExpandedHeight = detailAfter . height
fixture . view . debugUnhoverCard ( at : 1 )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( 0.06 ) )
let detailDuringClose = fixture . view . debugCardExpandedDetailPresentationHeights [ 1 ]
let followingSlotDuringCloseY = fixture . view . debugCardSlotPresentationFramesInPanel [ 2 ] . minY
XCTAssertGreaterThan ( detailDuringClose , 4 )
XCTAssertLessThan ( detailDuringClose , detailExpandedHeight - 4 )
XCTAssertGreaterThan (
followingSlotDuringCloseY ,
min ( followingSlotClosedY , followingSlotExpandedY ) + 4
)
XCTAssertLessThan (
followingSlotDuringCloseY ,
max ( followingSlotClosedY , followingSlotExpandedY ) - 4
)
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ ] )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-row " ] )
XCTAssertGreaterThan ( fixture . view . debugAnimatedCardLayoutChangeCount , layoutChangeCountAfterHover )
XCTAssertEqual ( fixture . view . debugCardExpandedDetailFramesInPanel [ 1 ] . height , 0 , accuracy : 0.5 )
}
2026-07-08 03:29:12 -04:00
func testHoverDuringCollapseUsesVisualCardUnderMouseInsteadOfCollapsedTargetSlot ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
for index in 0. . < 8 {
fixture . store . upsert ( makeTextItem ( " Visual hover row \( index ) " , store : fixture . store ) )
}
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
let selectedIndexBeforeHover = fixture . viewModel . selectedIndex
2026-07-08 03:29:12 -04:00
fixture . view . debugHoverCard ( at : 1 )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ 1 ] )
fixture . view . debugUnhoverCard ( at : 1 )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( 0.06 ) )
let visualRowFrame = fixture . view . debugCardSlotPresentationFramesInPanel [ 2 ]
let visualRowPoint = NSPoint ( x : visualRowFrame . midX , y : visualRowFrame . midY )
fixture . view . debugHoverCard ( at : 6 , mouseLocationInPanel : visualRowPoint )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ 2 ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . selectedIndex , selectedIndexBeforeHover )
2026-07-08 03:29:12 -04:00
}
2026-07-08 01:13:02 -04:00
func testToolbarControlsExposeVoiceOverActionHints ( ) {
let fixture = makePanelFixture ( )
XCTAssertEqual (
fixture . view . debugSearchFieldAccessibilityHelp ,
2026-07-09 19:32:57 -04:00
" Type to search clipboard history. Use the category icons beside the cards to filter results; Command-click combines categories. "
2026-07-08 01:13:02 -04:00
)
XCTAssertEqual (
fixture . view . debugAddCollectionButtonAccessibilityHelp ,
" Create a new Pinboard collection. Keyboard shortcut: Shift-Command-N. "
)
XCTAssertEqual (
fixture . view . debugToolbarButtonAccessibilityLabels ,
[ " Clear History " , " Settings " ]
)
XCTAssertEqual (
fixture . view . debugToolbarButtonAccessibilityHelps ,
[
" Clear history. " ,
" Open ClipBored settings. Keyboard shortcut: Command-Comma. "
]
)
XCTAssertEqual (
fixture . view . debugToolbarButtonAccessibilityHelps ,
[
" Clear history. " ,
" Open ClipBored settings. Keyboard shortcut: Command-Comma. "
]
)
}
2026-07-09 19:32:57 -04:00
func testActiveVerticalCardUsesSubtleDepthWithoutTranslation ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Neighbor card chrome " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Active card chrome " , store : fixture . store ) )
drainMainQueue ( )
fixture . viewModel . selectItem ( at : 0 )
fixture . view . debugHoverCard ( at : 0 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertGreaterThanOrEqual (
fixture . view . debugDocumentViewFrame . height ,
fixture . view . debugCardRailVisibleRect . height - 1
)
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugFirstCardShadowOpacity , 0 )
XCTAssertLessThanOrEqual ( fixture . view . debugFirstCardShadowOpacity , 0.12 )
XCTAssertLessThanOrEqual ( fixture . view . debugFirstCardShadowRadius , 12 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardLayerTranslationY , 0 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugFirstCardSlotTopOffset , 0 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSecondCardSlotTopOffset , 0 , accuracy : 0.5 )
XCTAssertGreaterThan ( fixture . view . debugFirstCardSlotZPosition , fixture . view . debugSecondCardSlotZPosition )
}
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 )
XCTAssertTrue ( fixture . view . debugDocumentViewIsCardStack )
XCTAssertGreaterThanOrEqual ( fixture . view . debugDocumentViewFrame . width , 292 )
XCTAssertGreaterThanOrEqual (
fixture . view . debugDocumentViewFrame . height ,
fixture . view . debugCardRailVisibleRect . height - 1
)
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 , [ " " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewChromes , [ " paper-preview " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewAccentHexes , [ " #F8B200 " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewFadePlacements , [ " none " ] )
}
func testMultiLineTextCardsShowRemainderBelowTitle ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Address: \n 399 The Embarcadero \n San Francisco " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardTextPreviewTitles , [ " Address: " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewBodies , [ " 399 The Embarcadero San Francisco " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewChromes , [ " paper-preview " ] )
XCTAssertEqual ( fixture . view . debugCardTextPreviewFadePlacements , [ " bottom " ] )
}
func testCompactRowsFitSideShelf ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , 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 . debugCardPresentations , [ " vertical-row " , " vertical-row " ] )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThanOrEqual ( fixture . view . debugCardSizes . first ? . width ? ? 0 , 240 )
2026-07-08 01:13:02 -04:00
XCTAssertLessThanOrEqual (
fixture . view . debugCardSizes . first ? . width ? ? 0 ,
fixture . view . debugCardRailVisibleRect . width
)
XCTAssertEqual ( fixture . view . debugCardSizes . first ? . height ? ? 0 , 56 , accuracy : 0.5 )
XCTAssertEqual (
fixture . view . debugCardSizes . last ? . width ? ? 0 ,
fixture . view . debugCardSizes . first ? . width ? ? 0 ,
accuracy : 0.5
)
XCTAssertEqual ( fixture . view . debugCardSizes . last ? . height ? ? 0 , 56 , accuracy : 0.5 )
XCTAssertLessThanOrEqual (
fixture . view . debugDocumentViewFrame . width ,
fixture . view . debugCardRailVisibleRect . width + 1
)
}
2026-07-09 19:32:57 -04:00
func testCompactRowsPrioritizeReadableTitleOverVerboseCharacterMetric ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 520 ) , display : true )
fixture . store . upsert ( makeTextItem ( String ( repeating : " a " , count : 1_234 ) , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Newest expanded row " , store : fixture . store ) )
fixture . viewModel . selectItem ( at : 0 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 2 ) , [ " vertical-row " , " vertical-row " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardCompactMetricTexts , [ " " , " " ] )
XCTAssertEqual ( fixture . view . debugCardCompactMetricHiddenStates , [ true , true ] )
2026-07-08 01:13:02 -04:00
}
2026-07-09 19:32:57 -04:00
func testCompactRowsKeepFileMetadataOutOfThePrimaryTitleLine ( ) throws {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 520 ) , display : true )
let fileURL = makeTempDirectory ( ) . appendingPathComponent ( " payload.bin " )
try Data ( repeating : 0x7A , count : 2_048 ) . write ( to : fileURL )
fixture . store . upsert ( makeItem ( kind : . file , displayText : " payload.bin " , payload : fileURL . path , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Newest expanded row " , store : fixture . store ) )
fixture . viewModel . selectItem ( at : 0 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 2 ) , [ " vertical-row " , " vertical-row " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardCompactMetricTexts , [ " " , " " ] )
XCTAssertEqual ( fixture . view . debugCardCompactMetricHiddenStates , [ true , true ] )
2026-07-08 01:13:02 -04:00
}
func testTallSideShelfUsesPasteStyleFocusedCard ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 640 ) , display : true )
fixture . store . upsert ( makeTextItem ( " Expanded shelf preview " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardDensity , " compact " )
XCTAssertEqual ( fixture . view . debugCardPresentations , [ " vertical-row " ] )
XCTAssertLessThanOrEqual (
fixture . view . debugCardSizes . first ? . width ? ? 0 ,
fixture . view . debugCardRailVisibleRect . width
)
XCTAssertEqual ( fixture . view . debugCardSizes . first ? . height ? ? 0 , 56 , accuracy : 0.5 )
XCTAssertGreaterThanOrEqual ( fixture . view . debugDocumentViewFrame . height , 72 )
}
func testVerticalPanelLayoutStacksCardsAndScrollsVertically ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 520 , height : 900 ) , display : true )
for index in 0. . < 12 {
fixture . store . upsert ( makeTextItem ( " Vertical shelf clip \( index ) " , store : fixture . store ) )
}
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugPanelLayout , " Side Shelf " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugItemsStackOrientation , . vertical )
XCTAssertTrue ( fixture . view . debugResizeHandleIsHidden )
XCTAssertEqual ( fixture . view . debugCardDensity , " compact " )
XCTAssertGreaterThan ( fixture . view . debugDocumentViewFrame . height , fixture . view . debugCardRailVisibleRect . height )
XCTAssertLessThanOrEqual ( fixture . view . debugDocumentViewFrame . width , fixture . view . debugCardRailVisibleRect . width + 1 )
}
2026-07-09 19:32:57 -04:00
func testVerticalShelfUsesSideCategoryRailAndFullHeightCards ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Vertical switch note " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/vertical " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " vertical-image " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugPanelLayout , " Side Shelf " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . bounds . width , 336 , accuracy : 1 )
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleWidth , 36 , accuracy : 0.5 )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCollectionRailHeight , 300 )
XCTAssertGreaterThanOrEqual ( fixture . view . debugCardSizes . first ? . width ? ? 0 , 240 )
XCTAssertLessThanOrEqual (
fixture . view . debugCollectionRailFrameInPanel . maxX + 8 ,
fixture . view . debugSelectedCardFrameInPanel . minX + 0.5
)
2026-07-08 01:13:02 -04:00
}
func testCardsShowQuickPasteNumberBadgesForFirstNineItems ( ) {
let fixture = makePanelFixture ( )
for index in 0. . < 10 {
fixture . store . upsert ( makeTextItem ( " Quick paste badge \( index ) " , store : fixture . store ) )
drainMainQueue ( )
}
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugVisibleCardCount , 10 )
XCTAssertEqual ( fixture . view . debugQuickPasteBadgeTexts , [ " 1 " , " 2 " , " 3 " , " 4 " , " 5 " , " 6 " , " 7 " , " 8 " , " 9 " ] )
}
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 testCardFooterShowsRemoteDeviceContextWhenAvailable ( ) {
let fixture = makePanelFixture ( )
var item = makeTextItem ( " Copied on another Mac " , store : fixture . store )
item . sourceApp = " Safari "
item . sourceDeviceName = " Studio Mac "
item . useCount = 1
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugFirstCardFooterSourceIsHidden )
XCTAssertEqual ( fixture . view . debugFirstCardFooterSourceText , " Safari on Studio Mac - Used once " )
}
func testCardFooterShowsRemoteDeviceWithoutSourceApp ( ) {
let fixture = makePanelFixture ( )
var item = makeTextItem ( " Device-only clip " , store : fixture . store )
item . sourceApp = nil
item . sourceDeviceName = " MacBook Pro "
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugFirstCardFooterSourceIsHidden )
XCTAssertEqual ( fixture . view . debugFirstCardFooterSourceText , " MacBook Pro " )
}
func testEditedTextStatusUsesActionTone ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Editable footer item " , store : fixture . store ) )
drainMainQueue ( )
fixture . viewModel . updateSelectedText ( to : " Edited footer item " )
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Updated text clip " )
2026-07-08 01:13:02 -04:00
}
2026-07-09 19:32:57 -04:00
func testValidationActionsPublishUsefulStatusMessages ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . viewModel . addVisibleItemsToStack ( )
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " No visible clips to stack " )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Stack warning note " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . viewModel . addSelectedItemsToStack ( )
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Added 1 selected clip to Stack " )
2026-07-08 01:13:02 -04:00
fixture . viewModel . addSelectedItemsToStack ( )
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Selected clips are already in Stack " )
2026-07-08 01:13:02 -04:00
}
func testPanelShellUsesTransparentLiquidGlassSurface ( ) {
let ( _ , view ) = makePanelWithPanelView ( )
drainMainQueue ( )
view . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( view . debugPanelMaterial , . hudWindow )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( view . debugPanelCornerRadius , 14 )
XCTAssertEqual ( view . debugPanelShadowOpacity , 0 )
2026-07-08 01:13:02 -04:00
XCTAssertGreaterThan ( view . debugPanelSurfaceAlpha , 0.10 )
XCTAssertLessThan ( view . debugPanelSurfaceAlpha , 0.25 )
XCTAssertGreaterThan ( view . debugPanelBorderAlpha , 0.30 )
}
func testWritesPasteStyleVisualSnapshotWhenRequested ( ) throws {
guard ProcessInfo . processInfo . environment [ " CLIPBORED_WRITE_VISUAL_SNAPSHOT " ] = = " 1 " else {
throw XCTSkip ( " Set CLIPBORED_WRITE_VISUAL_SNAPSHOT=1 to write a visual panel snapshot. " )
}
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
var code = makeItem (
kind : . code ,
displayText : " Swift Formatter " ,
payload : " func paste(_ item: ClipboardItem) { \n clipboard.write(item) \n } " ,
store : fixture . store
)
code . sourceApp = " Xcode "
var color = makeItem ( kind : . color , displayText : " #0A84FF " , payload : " #0A84FF " , store : fixture . store )
color . sourceApp = " Sketch "
var audio = makeItem ( kind : . audio , displayText : " Tame Impala " , payload : " /tmp/tame-impala.sound " , store : fixture . store )
audio . sourceApp = " Music "
let image = makeCachedImageItem ( store : fixture . store , cacheService : fixture . cacheService )
var text = makeTextItem ( " Address: \n 399 The Embarcadero \n San Francisco, CA 94105 \n United States " , store : fixture . store )
text . sourceApp = " Mail "
let linkID = UUID ( )
let linkPaths = try XCTUnwrap ( fixture . cacheService . cacheImage ( sampleImage ( ) , id : linkID ) )
let link = ClipboardItem (
id : linkID ,
kind : . url ,
displayText : " OMA's office furniture line " ,
payload : " https://kvellhome.com/lookbook " ,
payloadHash : fixture . store . hashString ( " https://kvellhome.com/lookbook " ) ,
createdAt : Date ( timeIntervalSinceNow : - 180 ) ,
lastUsedAt : Date ( timeIntervalSinceNow : - 180 ) ,
useCount : 0 ,
sourceApp : " Safari " ,
imagePath : linkPaths . full ,
thumbnailPath : linkPaths . thumb
)
for item in [ code , color , audio , image , text , link ] . reversed ( ) {
fixture . store . upsert ( item )
}
fixture . viewModel . selectItem ( at : 0 )
2026-07-09 19:32:57 -04:00
if ProcessInfo . processInfo . environment [ " CLIPBORED_VISUAL_SNAPSHOT_EXPAND_SEARCH " ] = = " 1 " {
fixture . view . debugExpandSearchPresentationForSnapshot ( )
}
2026-07-08 01:13:02 -04:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . displayIfNeeded ( )
let output = URL ( fileURLWithPath : ProcessInfo . processInfo . environment [ " CLIPBORED_VISUAL_SNAPSHOT_PATH " ] ? ? " build/visual-snapshots/paste-style-panel.png " )
try FileManager . default . createDirectory ( at : output . deletingLastPathComponent ( ) , withIntermediateDirectories : true )
try writeSnapshot ( of : fixture . view , to : output )
}
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 )
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 testFocusedCardKeepsKeyboardFocusWhenVisibleItemsReloadAboveIt ( ) {
let fixture = makePanelFixture ( )
var existing = makeTextItem ( " Keep keyboard focus " , store : fixture . store )
existing . createdAt = Date ( timeIntervalSince1970 : 100 )
existing . lastUsedAt = existing . createdAt
fixture . store . upsert ( existing )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 0 ] )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Keep keyboard focus " )
var newer = makeTextItem ( " Newer clip above focus " , store : fixture . store )
newer . createdAt = Date ( timeIntervalSince1970 : 200 )
newer . lastUsedAt = newer . createdAt
fixture . store . upsert ( newer )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Newer clip above focus " , " Keep keyboard focus " ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugActiveCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Keep keyboard focus " )
}
2026-07-09 19:32:57 -04:00
func testHoverExpansionDoesNotStealKeyboardSelectionOrNavigation ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Oldest hover note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Middle hover note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Newest hover note " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Newest hover note " )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 0 ] )
fixture . view . debugHoverCard ( at : 2 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Newest hover note " )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 0 ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugActiveCardIndexes , [ 2 ] )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ 2 ] )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-row " , " vertical-focus " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardSelectionInputSource , " keyboard " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardVisibleActionLabels ( at : 2 ) , [ ] )
2026-07-09 19:32:57 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 125 )
2026-07-08 01:13:02 -04:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Middle hover note " )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugActiveCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ ] )
XCTAssertEqual ( fixture . view . debugCardPresentations . prefix ( 3 ) , [ " vertical-row " , " vertical-focus " , " vertical-row " ] )
XCTAssertEqual ( fixture . view . debugCardSelectionInputSource , " keyboard " )
XCTAssertEqual ( fixture . view . debugCardVisibleActionLabels ( at : 2 ) , [ ] )
fixture . view . debugRefreshHoverCardWithoutMouseMovement ( at : 2 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Middle hover note " )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ ] )
XCTAssertEqual ( fixture . view . debugCardSelectionInputSource , " keyboard " )
fixture . view . debugHoverCard ( at : 2 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Middle hover note " )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugHoveredCardIndexes , [ 2 ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardSelectionInputSource , " keyboard " )
2026-07-08 01:13:02 -04:00
}
2026-07-09 19:32:57 -04:00
func testCollectionRailUsesCompactSideIconsAndTracksSelection ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCollectionTitles , [ " Clipboard " ] )
XCTAssertEqual ( fixture . view . debugCollectionLeadingSymbols , [ " doc.on.clipboard " ] )
XCTAssertEqual ( fixture . view . debugCollectionChipWidths . count , 1 )
XCTAssertEqual ( fixture . view . debugCollectionChipWidths . first ? ? 0 , 34 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugCollectionChipLabelHiddenStates , [ true ] )
XCTAssertEqual ( fixture . view . debugCollectionChipCountLabelHiddenStates , [ true ] )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCollectionTitles , [ " Clipboard " , " Frequent " , " Links " ] )
XCTAssertEqual ( fixture . view . debugCollectionChipLabelHiddenStates , [ true , true , true ] )
XCTAssertEqual ( fixture . view . debugCollectionChipCountLabelHiddenStates , [ true , true , true ] )
fixture . view . debugMouseDownCollectionChip ( . links )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Links " )
}
func testMouseSelectingCollectionChipMovesFocusAndLeavesSingleSelection ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . mostRecent ) )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Clipboard " ] )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Clipboard " ] )
fixture . view . debugMouseDownCollectionChip ( . links )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Links " )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Links " ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Links " ] )
}
func testCommandClickingCategoryChipsCombinesFilters ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Text category note " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/category " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " image payload " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
fixture . view . debugHoverCollectionChip ( . text )
2026-07-08 01:13:02 -04:00
fixture . view . debugCommandMouseDownCollectionChip ( . text )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " ] )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Text category note " ] )
2026-07-09 19:32:57 -04:00
fixture . view . debugHoverCollectionChip ( . links )
2026-07-08 01:13:02 -04:00
fixture . view . debugCommandMouseDownCollectionChip ( . links )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " , " Links " ] )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " https://example.com/category " , " Text category note " ] )
}
2026-07-09 19:32:57 -04:00
func testSelectingCategoryLeavesOnlyTheNewCategoryVisuallyHighlighted ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Text category note " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/category " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . mostRecent ) )
XCTAssertGreaterThan ( fixture . view . debugCollectionChipBackgroundAlpha ( . mostRecent ) , 0.4 )
fixture . view . debugMouseDownCollectionChip ( . text )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " ] )
XCTAssertLessThanOrEqual ( fixture . view . debugCollectionChipBackgroundAlpha ( . mostRecent ) , 0.1 )
XCTAssertGreaterThan ( fixture . view . debugCollectionChipBackgroundAlpha ( . text ) , 0.4 )
}
func testOpeningShortcutModifiersDoNotAccidentallyCombineCategorySelection ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Text category note " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/category " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugMouseDownCollectionChip ( . mostUsed )
fixture . view . debugMouseDownCollectionChip ( . text , modifiers : [ . command , . shift ] )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " ] )
fixture . view . debugMouseDownCollectionChip ( . links , modifiers : . command )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " , " Links " ] )
}
func testCommandSpaceCombinesFocusedCategoryChips ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Text category note " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com/category " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . text ) )
fixture . view . debugPressFocusedResponderKeyCode ( 49 , modifiers : . command )
drainMainQueue ( )
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . links ) )
fixture . view . debugPressFocusedResponderKeyCode ( 49 , modifiers : . command )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Text " , " Links " ] )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " https://example.com/category " , " Text category note " ] )
}
2026-07-08 01:13:02 -04:00
func testCollectionRailChipsAreKeyboardFocusableAndVoiceOverDescriptive ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugCollectionChipAcceptsFirstResponder ,
Array ( repeating : true , count : fixture . view . debugCollectionTitles . count )
)
XCTAssertEqual (
fixture . view . debugCollectionCountLabelHiddenStates ,
2026-07-09 19:32:57 -04:00
[ true , true , true ]
2026-07-08 01:13:02 -04:00
)
XCTAssertEqual ( fixture . view . debugCollectionChipAccessibilityLabels . first , " Clipboard, selected, 1 clip " )
XCTAssertEqual (
fixture . view . debugCollectionChipAccessibilityHelps . first ,
2026-07-09 19:32:57 -04:00
" Press Return or Space to show Clipboard; Command-Return or Command-Space combines categories. Use Left/Right to move between categories, Up/Down to move between clips, and Home/End to jump between categories. "
2026-07-08 01:13:02 -04:00
)
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . mostUsed ) )
fixture . view . debugPressFocusedResponderWithSpace ( )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Frequent " )
XCTAssertTrue ( fixture . view . debugCollectionChipAccessibilityLabels . contains ( " Frequent, selected, 1 clip " ) )
XCTAssertTrue (
fixture . view . debugCollectionChipAccessibilityHelps . contains (
2026-07-09 19:32:57 -04:00
" Press Return or Space to show Frequent; Command-Return or Command-Space combines categories. Use Left/Right to move between categories, Up/Down to move between clips, and Home/End to jump between categories. "
2026-07-08 01:13:02 -04:00
)
)
fixture . view . debugMouseDownCollectionChip ( . links )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Links " )
}
2026-07-09 19:32:57 -04:00
func testCollectionRailUsesHorizontalArrowsForChipsAndVerticalArrowsForCards ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
var clientItem = makeTextItem ( " Client collection item " , store : fixture . store )
clientItem . collectionName = " Client Work "
fixture . store . upsert ( clientItem )
fixture . store . upsert ( makeItem ( kind : . url , text : " https://example.com " , store : fixture . store ) )
fixture . store . upsert ( makeItem ( kind : . image , text : " image payload " , store : fixture . store ) )
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 ( . mostRecent ) )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( fixture . viewModel . selectedIndex , 0 )
fixture . view . debugPressFocusedResponderKeyCode ( 124 )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . selectedIndex , 0 )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Frequent " ] )
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 123 )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( fixture . viewModel . selectedIndex , 0 )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Clipboard " ] )
fixture . view . debugPressFocusedResponderKeyCode ( 125 )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( fixture . viewModel . selectedIndex , 1 )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . mostRecent ) )
fixture . view . debugPressFocusedResponderKeyCode ( 119 )
drainMainQueue ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Stack " ] )
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 ( . mostRecent ) )
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 testTypingFromFocusedCollectionChipAppendsSearchTokenBoundary ( ) {
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 ( " type:text " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCollectionChip ( . mostRecent ) )
fixture . view . debugTypeFocusedResponder ( " q " , keyCode : 12 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " type:text 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 ( . mostRecent ) )
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 . debugKeyboardFocusedCollectionTitles , [ " 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 testToolbarDoesNotExposeNewTextClipControl ( ) {
let fixture = makePanelFixture ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugToolbarButtonAccessibilityLabels . contains ( " New text clip " ) )
XCTAssertEqual ( fixture . view . debugToolbarButtonAccessibilityLabels , [ " Clear History " , " Settings " ] )
}
func testClearHistoryMenuClearsRecentRangesAndAllTime ( ) {
let fixture = makePanelFixture ( )
var older = makeTextItem ( " Older than a day " , store : fixture . store )
older . createdAt = Date ( ) . addingTimeInterval ( - 90_000 )
older . lastUsedAt = older . createdAt
var recent = makeTextItem ( " Recent clip " , store : fixture . store )
recent . createdAt = Date ( ) . addingTimeInterval ( - 3_600 )
recent . lastUsedAt = recent . createdAt
fixture . store . upsert ( older )
fixture . store . upsert ( recent )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugClearHistoryMenuTitles , [ " Past Hour " , " Past Day " , " All Time " ] )
fixture . view . debugPerformClearHistoryMenuItem ( titled : " Past Day " )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Older than a day " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Cleared 1 clip " )
2026-07-08 01:13:02 -04:00
fixture . view . debugPerformClearHistoryMenuItem ( titled : " All Time " )
drainMainQueue ( )
XCTAssertTrue ( fixture . viewModel . visibleItems . isEmpty )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Cleared 1 clip " )
2026-07-08 01:13:02 -04:00
}
func testCollectionFilteredCardsUseStoredCollectionHeaderColor ( ) {
let fixture = makePanelFixture ( )
fixture . viewModel . createCollection ( named : " Research Stack " , colorHex : " #0A9EB8 " )
var item = makeTextItem ( " Collect this note " , store : fixture . store )
item . collectionName = " Research Stack "
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Collect this note " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderTitle , " Collect this note " )
XCTAssertEqual ( fixture . view . debugFirstCardHeaderSubtitle , " Text • Ghostty • Just now " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderColorHex , " #0A9EB8 " )
XCTAssertEqual ( fixture . view . debugCustomCollectionColorHexes [ " Research Stack " ] , " #0A9EB8 " )
XCTAssertEqual ( fixture . view . debugFirstCardFooterDetailText , " 17 characters " )
}
2026-07-09 19:32:57 -04:00
func testCompactCardsUseFullColorHeaderSurface ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Full color card " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardHeaderSurfaceColorHex ,
fixture . view . debugFirstCardHeaderColorHex
)
XCTAssertEqual ( fixture . view . debugFirstCardHeaderSurfaceAlpha , 0.96 , accuracy : 0.01 )
}
2026-07-08 01:13:02 -04:00
func testCardHeaderUsesPasteStyleRelativeAgeText ( ) {
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 ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderTitle , " Readable age " )
XCTAssertEqual ( fixture . view . debugFirstCardHeaderSubtitle , " Text • Ghostty • 3 minutes ago " )
2026-07-08 01:13:02 -04:00
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 ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderSubtitle , " Text • Ghostty • 3 minutes ago " )
2026-07-08 01:13:02 -04:00
}
func testCollectionChipsExposeManagementMenuActions ( ) {
let fixture = makePanelFixture ( )
fixture . viewModel . createCollection ( named : " Research Stack " , colorHex : " #0A9EB8 " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugCustomCollectionMenuTitles ( named : " Research Stack " ) ,
[ " Edit Collection... " , " Export Pinboard... " , " - " , " Delete Collection " ]
)
XCTAssertEqual (
fixture . view . debugCustomCollectionAccessibilityHelp ( named : " Research Stack " ) ,
2026-07-09 19:32:57 -04:00
" Press Return or Space to show Research Stack; Command-Return or Command-Space combines categories. Use Left/Right to move between categories, Up/Down to move between clips, and Home/End to jump between categories. Open the context menu to edit, export, or delete this Pinboard. "
2026-07-08 01:13:02 -04:00
)
}
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 ( )
XCTAssertTrue ( fixture . view . debugFocusCustomCollectionChip ( named : " Research Stack " ) )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Research Stack " ] )
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 . debugKeyboardFocusedCollectionTitles , [ " Product Research " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderTitle , " Collect this note " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardHeaderColorHex , " #3366FF " )
fixture . view . debugDeleteCollection ( named : " Product Research " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCustomCollectionTitles , [ ] )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Clipboard " )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCollectionTitles , [ " Clipboard " ] )
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 , [ ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionRailWidth , 0 )
XCTAssertFalse ( fixture . view . debugFirstCardFooterDetailIsHidden )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderBadgeIsHidden )
XCTAssertEqual ( fixture . view . debugCardPresentations , [ " vertical-focus " ] )
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 " ]
)
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 ( )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . first ? . kind , . file )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionRailWidth , 0 )
XCTAssertFalse ( fixture . view . debugFirstCardFooterDetailIsHidden )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderBadgeIsHidden )
XCTAssertEqual ( fixture . view . debugCardPresentations . first , " vertical-focus " )
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 testLargeCollectionReloadKeepsHiddenCardActionsLazy ( ) {
let fixture = makePanelFixture ( )
fixture . settings . maxHistoryItems = 60
fixture . settings . ensureCollection ( named : " Client Work " )
fixture . view . beginOpeningTransition ( )
for index in 0. . < 30 {
var item = makeTextItem ( " Client clip \( index ) " , store : fixture . store )
item . collectionName = " Client Work "
fixture . store . upsert ( item )
}
drainMainQueue ( )
fixture . view . finishOpeningTransition ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardItemCount , 30 )
XCTAssertLessThan ( fixture . view . debugCardSlotCount , 30 )
XCTAssertGreaterThan ( fixture . view . debugCardSlotCount , 0 )
XCTAssertLessThan ( fixture . view . debugVisibleCardCount , 30 )
XCTAssertGreaterThan ( fixture . view . debugVisibleCardCount , 0 )
XCTAssertEqual ( fixture . view . debugConstructedActionButtonCount , 0 )
fixture . viewModel . selectCollection ( named : " Client Work " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . count , 30 )
XCTAssertEqual ( fixture . view . debugCardItemCount , 30 )
XCTAssertLessThan ( fixture . view . debugCardSlotCount , 30 )
XCTAssertGreaterThan ( fixture . view . debugCardSlotCount , 0 )
XCTAssertLessThan ( fixture . view . debugVisibleCardCount , 30 )
XCTAssertGreaterThan ( fixture . view . debugVisibleCardCount , 0 )
XCTAssertLessThan ( fixture . view . debugLastSynchronousCardRenderCount , 30 )
XCTAssertGreaterThan ( fixture . view . debugLastSynchronousCardRenderCount , 0 )
XCTAssertEqual ( fixture . view . debugConstructedActionButtonCount , 0 )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
fixture . view . debugScrollCardRailVertically ( deltaY : - 10_000 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertGreaterThan ( fixture . view . debugCardRailVisibleRect . minY , 0 )
XCTAssertEqual ( fixture . view . debugCardItemCount , 30 )
XCTAssertLessThan ( fixture . view . debugCardSlotCount , 30 )
XCTAssertLessThan ( fixture . view . debugVisibleCardCount , 30 )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertEqual ( fixture . view . debugConstructedActionButtonCount , 0 )
}
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 , [ ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( fixture . view . debugCardExpansionAnimationDuration + 0.04 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionRailWidth , 0 )
XCTAssertEqual ( fixture . view . debugCardPresentations , [ " vertical-focus " ] )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderBadgeIsHidden )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThanOrEqual ( fixture . view . debugCardSizes . first ? . width ? ? 0 , 520 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardSizes . first ? . height ? ? 0 , 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 ) )
2026-06-30 09:44:45 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Older text card " )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
XCTAssertEqual ( fixture . view . debugCardBorderWidths [ 1 ] , 0.8 , accuracy : 0.01 )
XCTAssertEqual ( fixture . view . debugCardAccessibilityValues [ 1 ] , " Selected " )
XCTAssertEqual ( fixture . view . debugCardAccessibilityHelps [ 1 ] , " Press Return to paste. Space opens Quick Look. Command-R renames; Command-E edits text. Delete removes selected clips; Command-Z restores them. " )
2026-06-30 09:44:45 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderWithReturn ( )
2026-06-30 09:44:45 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Copied " )
2026-06-30 09:44:45 -07:00
}
2026-07-08 01:13:02 -04:00
func testFocusedCardCommandShortcutsOpenRenameAndEditFlows ( ) {
2026-06-30 01:34:05 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . view . debugSuppressClipEditDialogs ( )
fixture . store . upsert ( makeTextItem ( " Shortcut editable text " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:34:05 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 01:34:05 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 15 , modifiers : . command )
2026-06-30 01:34:05 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugRenameClipRequestCount , 1 )
XCTAssertEqual ( fixture . view . debugEditClipRequestCount , 0 )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Shortcut editable text " )
2026-06-30 01:34:05 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 14 , modifiers : . command )
2026-06-30 01:34:05 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugRenameClipRequestCount , 1 )
XCTAssertEqual ( fixture . view . debugEditClipRequestCount , 1 )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Shortcut editable text " )
2026-06-30 01:34:05 -07:00
}
2026-07-08 01:13:02 -04:00
func testFocusedNonTextCardIgnoresCommandEditShortcut ( ) {
2026-06-30 10:22:09 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . view . debugSuppressClipEditDialogs ( )
fixture . store . upsert ( makeItem ( kind : . file , text : " /tmp/report.txt " , store : fixture . store ) )
2026-06-30 10:22:09 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 10:22:09 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 14 , modifiers : . command )
2026-06-30 10:22:09 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugEditClipRequestCount , 0 )
XCTAssertEqual ( fixture . view . debugRenameClipRequestCount , 0 )
2026-06-30 10:22:09 -07:00
}
2026-07-08 01:13:02 -04:00
func testFocusedCardDeleteRemovesSelectionAndCommandZRestoresIt ( ) {
2026-06-30 04:01:57 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
var oldest = makeTextItem ( " Oldest focused delete " , store : fixture . store )
oldest . createdAt = Date ( timeIntervalSince1970 : 100 )
oldest . lastUsedAt = oldest . createdAt
var middle = makeTextItem ( " Middle focused delete " , store : fixture . store )
middle . createdAt = Date ( timeIntervalSince1970 : 200 )
middle . lastUsedAt = middle . createdAt
var newest = makeTextItem ( " Newest focused delete " , store : fixture . store )
newest . createdAt = Date ( timeIntervalSince1970 : 300 )
newest . lastUsedAt = newest . createdAt
fixture . store . upsert ( oldest )
fixture . store . upsert ( middle )
fixture . store . upsert ( newest )
2026-06-30 04:01:57 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
fixture . viewModel . selectItem ( at : 0 )
fixture . viewModel . selectItem ( at : 2 , mode : . toggle )
2026-06-30 04:01:57 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 , 2 ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 04:01:57 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 51 )
2026-06-30 04:01:57 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Middle focused delete " ] )
XCTAssertEqual ( fixture . viewModel . statusMessage , " Deleted 2 clips " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Deleted 2 clips " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 ] )
2026-06-30 04:01:57 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
fixture . view . debugPressFocusedResponderKeyCode ( 6 , modifiers : . command )
2026-06-30 04:01:57 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual (
fixture . viewModel . visibleItems . map ( \ . payload ) ,
[ " Newest focused delete " , " Middle focused delete " , " Oldest focused delete " ]
)
XCTAssertEqual ( fixture . viewModel . statusMessage , " Restored 2 clips " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Restored 2 clips " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 , 2 ] )
2026-06-30 04:01:57 -07:00
}
2026-07-08 01:13:02 -04:00
func testFocusedCardCopyAndPlainTextPasteShortcutsUseSelection ( ) {
2026-06-30 01:12:19 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . store . upsert ( makeTextItem ( " Focused shortcut payload " , store : fixture . store ) )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
2026-06-30 10:07:36 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 01:12:19 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 8 , modifiers : . command )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Copied " )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Focused shortcut payload " )
2026-06-30 10:07:36 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
fixture . view . debugPressFocusedResponderKeyCode ( 36 , modifiers : . shift )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . statusMessage , " Copied Plain Text " )
2026-06-30 01:12:19 -07:00
2026-06-30 10:07:36 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
fixture . view . debugPressFocusedResponderKeyCode ( 9 , modifiers : [ . command , . shift ] )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . statusMessage , " Copied Plain Text " )
2026-06-30 01:12:19 -07:00
}
2026-07-08 01:13:02 -04:00
func testFocusedCardCommandPreviewOpenAndShowInClipboardShortcuts ( ) {
2026-06-30 09:54:59 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . view . debugSuppressClipOpenActions ( )
let url = makeItem ( kind : . url , displayText : " Release notes " , payload : " https://example.com/releases " , store : fixture . store )
var note = makeTextItem ( " Filtered release note " , store : fixture . store )
note . createdAt = Date ( timeIntervalSince1970 : 100 )
note . lastUsedAt = note . createdAt
fixture . store . upsert ( note )
fixture . store . upsert ( url )
fixture . view . debugSetSearchFieldText ( " release " )
2026-06-30 09:54:59 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " https://example.com/releases " , " Filtered release note " ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
2026-06-30 09:54:59 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 16 , modifiers : . command )
2026-06-30 08:56:45 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . previewProbe . requestCount , 1 )
fixture . view . debugPressFocusedResponderKeyCode ( 31 , modifiers : . command )
2026-06-30 08:56:45 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugOpenClipRequestCount , 1 )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " https://example.com/releases " )
2026-06-30 08:56:45 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 5 , modifiers : . command )
2026-06-30 08:56:45 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 08:56:45 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugShowInClipboardRequestCount , 1 )
XCTAssertEqual ( fixture . viewModel . statusMessage , " Showing in Clipboard " )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " https://example.com/releases " )
2026-06-30 08:56:45 -07:00
}
2026-06-30 09:33:00 -07:00
func testFocusedTextCardSpaceOpensQuickLookInsteadOfPasting ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Preview this text without pasting " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . view . debugPressFocusedResponderWithSpace ( )
drainMainQueue ( )
XCTAssertEqual ( fixture . previewProbe . requestCount , 1 )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Preview this text without pasting " )
XCTAssertEqual ( fixture . viewModel . statusMessage , " " )
}
2026-06-30 09:40:04 -07:00
func testTypingFromFocusedCardStartsSearch ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Alpha note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Quantum card " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
fixture . view . debugTypeFocusedResponder ( " q " , keyCode : 12 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " q " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Quantum card " ] )
}
2026-07-08 01:13:02 -04:00
func testTypingFromFocusedCardAppendsSearchTokenBoundary ( ) {
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 ( " type:text " )
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 , " type:text q " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Quantum card " ] )
}
2026-06-30 09:44:45 -07:00
func testKeyboardCancelClearsSearchFromFocusedCard ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Alpha note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Quantum card " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugSetSearchFieldText ( " quantum " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Quantum card " ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
XCTAssertTrue ( fixture . view . clearSearchForKeyboardCancel ( ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Quantum card " , " Alpha note " ] )
XCTAssertFalse ( fixture . view . clearSearchForKeyboardCancel ( ) )
}
2026-07-08 01:13:02 -04:00
func testKeyboardCancelClearsWhitespaceOnlySearchBeforeClosing ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Whitespace search note " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugSetSearchFieldText ( " " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Whitespace search note " ] )
XCTAssertTrue ( fixture . view . clearSearchForKeyboardCancel ( ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . isSearchFieldEditing )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Whitespace search note " ] )
XCTAssertFalse ( fixture . view . clearSearchForKeyboardCancel ( ) )
}
2026-06-30 08:56:45 -07:00
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 ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardAccessibilityHelps . first , " Press Return to paste. Space opens Quick Look. Command-R renames; Command-E edits text. Delete removes selected clips; Command-Z restores them. " )
2026-06-30 08:56:45 -07:00
fixture . view . debugPressFocusedResponderWithSpace ( )
drainMainQueue ( )
XCTAssertEqual ( fixture . previewProbe . requestCount , 1 )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " https://example.com/read " )
}
2026-06-30 09:19:18 -07:00
func testFocusedCardsSupportShelfNavigationKeys ( ) {
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 760 ) , display : true )
2026-06-30 09:19:18 -07:00
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 ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . selectedIndex , 0 )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 0 ] )
fixture . view . debugPressFocusedResponderKeyCode ( 125 )
drainMainQueue ( )
2026-06-30 09:19:18 -07:00
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 ] )
2026-07-09 19:32:57 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 126 )
2026-06-30 09:19:18 -07:00
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . selectedIndex , 0 )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 0 ] )
2026-07-08 01:13:02 -04:00
2026-07-09 19:32:57 -04:00
fixture . view . debugPressFocusedResponderKeyCode ( 125 , modifiers : . shift )
2026-07-08 01:13:02 -04:00
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . selectedIndex , 1 )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 , 1 ] )
XCTAssertEqual ( fixture . view . debugActiveCardIndexes , [ 1 ] )
fixture . view . debugPressFocusedResponderKeyCode ( 0 , modifiers : . command )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , Array ( 0. . < 8 ) )
XCTAssertEqual ( fixture . view . debugKeyboardFocusedCardIndexes , [ 1 ] )
2026-06-30 09:19:18 -07:00
}
2026-06-30 01:12:19 -07:00
func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable ( ) {
let fixture = makePanelFixture ( )
2026-06-30 16:56:04 -07:00
var item = makeItem ( kind : . url , text : " https://example.com " , store : fixture . store )
item . sourceApp = nil
fixture . store . upsert ( item )
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 01:12:19 -07:00
XCTAssertEqual ( fixture . view . debugCardHeaderBadgeSymbols , [ " link " ] )
2026-06-30 16:56:04 -07:00
XCTAssertEqual ( fixture . view . debugCardHeaderBadgeTexts , [ " " ] )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFirstCardHeaderMaskedCorners . contains ( . layerMinXMaxYCorner ) )
XCTAssertTrue ( fixture . view . debugFirstCardHeaderMaskedCorners . contains ( . layerMaxXMaxYCorner ) )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderMaskedCorners . contains ( . layerMinXMinYCorner ) )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderMaskedCorners . contains ( . layerMaxXMinYCorner ) )
XCTAssertTrue ( fixture . view . debugFirstCardHeaderBadgeMaskedCorners . contains ( . layerMinXMinYCorner ) )
XCTAssertTrue ( fixture . view . debugFirstCardHeaderBadgeMaskedCorners . contains ( . layerMinXMaxYCorner ) )
XCTAssertTrue ( fixture . view . debugFirstCardHeaderBadgeMaskedCorners . contains ( . layerMaxXMaxYCorner ) )
XCTAssertFalse ( fixture . view . debugFirstCardHeaderBadgeMaskedCorners . contains ( . layerMaxXMinYCorner ) )
XCTAssertGreaterThanOrEqual (
fixture . view . debugFirstCardHeaderBadgeContentFrame . width ,
2026-07-09 19:32:57 -04:00
fixture . view . debugFirstCardHeaderBadgeFrame . width * 0.55
2026-07-08 01:13:02 -04:00
)
}
func testCardHeaderSourceAppIconFillsBadgeTile ( ) throws {
let fixture = makePanelFixture ( )
var item = makeTextItem ( " Copied from Finder " , store : fixture . store )
item . sourceApp = " Finder "
item . sourceAppBundleId = " com.apple.finder "
guard NSWorkspace . shared . urlForApplication ( withBundleIdentifier : " com.apple.finder " ) != nil else {
throw XCTSkip ( " Finder app icon is unavailable in this environment. " )
}
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
let badgeFrame = fixture . view . debugFirstCardHeaderBadgeFrame
let contentFrame = fixture . view . debugFirstCardHeaderBadgeContentFrame
XCTAssertEqual ( fixture . view . debugCardHeaderBadgeTexts , [ " " ] )
XCTAssertLessThan ( contentFrame . minX , badgeFrame . minX )
XCTAssertLessThan ( contentFrame . minY , badgeFrame . minY )
XCTAssertGreaterThan ( contentFrame . maxX , badgeFrame . maxX )
XCTAssertGreaterThan ( contentFrame . maxY , badgeFrame . maxY )
XCTAssertEqual ( contentFrame . midX , badgeFrame . midX , accuracy : 0.5 )
XCTAssertEqual ( contentFrame . midY , badgeFrame . midY , accuracy : 0.5 )
2026-06-30 16:56:04 -07:00
}
func testCardHeaderUsesSourceMonogramWhenAppIconIsUnavailable ( ) {
let fixture = makePanelFixture ( )
var item = makeTextItem ( " Copied from a named app " , store : fixture . store )
item . sourceApp = " Arc Browser "
fixture . store . upsert ( item )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugCardHeaderBadgeTexts , [ " AB " ] )
2026-06-30 01:12:19 -07:00
}
func testCollectionRailShowsLiveCounts ( ) {
let fixture = makePanelFixture ( )
var pinned = makeTextItem ( " Pinned note " , store : fixture . store )
pinned . isPinned = true
let rich = makeItem ( kind : . richText , text : " Rich note " , store : fixture . store )
let link = makeItem ( kind : . url , text : " https://example.com/releases " , store : fixture . store )
let image = makeItem ( kind : . image , text : " image payload " , store : fixture . store )
2026-07-01 14:52:54 -07:00
let color = makeItem ( kind : . color , displayText : " #0A84FF " , payload : " #0A84FF " , store : fixture . store )
2026-06-30 01:12:19 -07:00
let audio = makeItem ( kind : . audio , text : " audio payload " , store : fixture . store )
2026-07-01 15:39:43 -07:00
let video = makeItem ( kind : . video , text : " /tmp/movie.mp4 " , store : fixture . store )
2026-06-30 01:12:19 -07:00
let file = makeItem ( kind : . file , text : " /tmp/report.pdf " , store : fixture . store )
2026-07-01 15:12:49 -07:00
let code = makeItem (
kind : . code ,
displayText : " Swift Snippet " ,
payload : " func greet(name: String) -> String { \n return \" Hi \\ (name) \" \n } " ,
store : fixture . store
)
2026-06-30 01:12:19 -07:00
2026-07-01 15:39:43 -07:00
[ pinned , rich , link , image , color , audio , video , file , code ] . forEach {
2026-06-30 01:12:19 -07:00
fixture . store . upsert ( $0 )
drainMainQueue ( )
}
2026-07-01 15:39:43 -07:00
XCTAssertEqual ( fixture . viewModel . visibleItems . count , 9 )
XCTAssertEqual ( ClipboardSortMode . allCases . map { fixture . viewModel . collectionCount ( for : $0 ) } , [ 9 , 9 , 3 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] )
2026-07-08 01:13:02 -04:00
let countSummary = fixture . viewModel . collectionCountSummary ( )
XCTAssertEqual ( ClipboardSortMode . allCases . map { countSummary . count ( for : $0 ) } , [ 9 , 9 , 3 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] )
2026-07-01 15:39:43 -07:00
XCTAssertEqual ( fixture . view . debugCollectionCounts , [ 9 , 9 , 3 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCollectionCountLabelHiddenStates , Array ( repeating : true , count : 11 ) )
2026-06-30 01:12:19 -07:00
}
func testCollectionRailShowsAssignedCollections ( ) {
let fixture = makePanelFixture ( )
var link = makeItem ( kind : . url , text : " https://example.com/read " , store : fixture . store )
link . collectionName = " Useful Links "
var note = makeTextItem ( " Meeting note " , store : fixture . store )
note . collectionName = " Important Notes "
var file = makeItem ( kind : . file , text : " /tmp/client-brief.pdf " , store : fixture . store )
file . collectionName = " Client Work "
fixture . store . upsert ( link )
fixture . store . upsert ( note )
fixture . store . upsert ( file )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugCustomCollectionTitles , [ " Useful Links " , " Important Notes " , " Client Work " ] )
XCTAssertEqual ( fixture . view . debugCustomCollectionCounts , [ 1 , 1 , 1 ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCustomCollectionCountLabelHiddenStates , [ true , true , true ] )
let countSummary = fixture . viewModel . collectionCountSummary ( )
XCTAssertEqual ( [ " Useful Links " , " Important Notes " , " Client Work " ] . map { countSummary . count ( named : $0 ) } , [ 1 , 1 , 1 ] )
2026-06-30 01:12:19 -07:00
fixture . viewModel . selectCollection ( named : " Useful Links " )
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " https://example.com/read " ] )
}
2026-06-30 03:02:29 -07:00
func testCardsCanDropOntoCollectionChipsToOrganize ( ) {
let fixture = makePanelFixture ( )
var existing = makeTextItem ( " Existing client note " , store : fixture . store )
existing . collectionName = " Client Work "
fixture . store . upsert ( existing )
let dropped = makeTextItem ( " Drop this note " , store : fixture . store )
fixture . store . upsert ( dropped )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCustomCollectionTitles , [ " Client Work " ] )
XCTAssertEqual ( fixture . view . debugCustomCollectionDropTargets , [ " Client Work " ] )
XCTAssertEqual ( fixture . viewModel . visibleItems . first ? . id , dropped . id )
fixture . view . debugDropFirstCard ( onCollectionNamed : " Client Work " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . viewModel . statusMessage , " Added to Client Work " )
XCTAssertEqual ( fixture . view . debugCustomCollectionCounts , [ 2 ] )
fixture . viewModel . selectCollection ( named : " Client Work " )
drainMainQueue ( )
XCTAssertEqual ( Set ( fixture . viewModel . visibleItems . map ( \ . payload ) ) , [ " Existing client note " , " Drop this note " ] )
}
2026-06-30 01:12:19 -07:00
func testCollectionRailUsesScrollableDocumentForCrowdedCustomCollections ( ) {
let fixture = makePanelFixture ( )
2026-06-30 09:02:19 -07:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
2026-06-30 01:12:19 -07:00
let names = [
" Client Work " ,
" Research Archive " ,
" Launch Planning " ,
" Design QA " ,
" Product References " ,
" Reading Stack " ,
" Invoices " ,
" Hiring Pipeline "
]
for name in names {
var item = makeTextItem ( " Collection item \( name ) " , store : fixture . store )
item . collectionName = name
fixture . store . upsert ( item )
}
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleWidth , 36 , accuracy : 0.5 )
XCTAssertFalse ( fixture . view . debugCollectionRailHasHorizontalScroller )
XCTAssertGreaterThan ( fixture . view . debugCollectionRailHeight , 300 )
2026-06-30 01:12:19 -07:00
XCTAssertGreaterThan (
2026-07-09 19:32:57 -04:00
fixture . view . debugCollectionRailContentFrameInPanel . height ,
fixture . view . debugCollectionRailVisibleRect . height
2026-06-30 01:12:19 -07:00
)
XCTAssertTrue ( fixture . view . debugCustomCollectionTitles . contains ( " Client Work " ) )
XCTAssertTrue ( fixture . view . debugCustomCollectionTitles . contains ( " Product References " ) )
2026-06-30 09:02:19 -07:00
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleRect . minY , 0 , accuracy : 0.5 )
2026-06-30 09:02:19 -07:00
fixture . view . debugScrollCollectionRailVertically ( deltaY : - 220 )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCollectionRailVisibleRect . minY , 0 )
2026-06-30 09:02:19 -07:00
fixture . view . debugScrollCollectionRailVertically ( deltaY : 10_000 )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleRect . minY , 0 , accuracy : 0.5 )
2026-06-30 01:12:19 -07:00
}
2026-06-30 01:29:36 -07:00
func testSelectionScrollsCardRailToKeepSelectedCardVisible ( ) {
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 520 ) , display : true )
2026-06-30 01:29:36 -07:00
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 ( )
2026-07-08 01:13:02 -04:00
XCTAssertLessThanOrEqual ( fixture . view . debugCardRailVisibleRect . minY , 1 )
2026-06-30 01:29:36 -07:00
fixture . viewModel . selectItem ( at : fixture . viewModel . visibleItems . count - 1 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
let visibleRect = fixture . view . debugCardRailVisibleRect
let selectedFrame = fixture . view . debugSelectedCardFrameInDocument
2026-07-08 01:13:02 -04:00
XCTAssertGreaterThan ( visibleRect . minY , 0 )
XCTAssertLessThanOrEqual ( selectedFrame . minY , visibleRect . maxY )
XCTAssertGreaterThanOrEqual ( visibleRect . maxY + 1 , selectedFrame . maxY )
2026-06-30 01:29:36 -07:00
fixture . viewModel . selectItem ( at : 0 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertLessThanOrEqual ( fixture . view . debugCardRailVisibleRect . minY , 1 )
2026-06-30 01:29:36 -07:00
}
2026-07-08 01:13:02 -04:00
func testVerticalWheelPansSideCardListAndClamps ( ) {
2026-06-30 09:02:19 -07:00
let fixture = makePanelFixture ( )
2026-07-08 01:13:02 -04:00
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 336 , height : 520 ) , display : true )
2026-06-30 09:02:19 -07:00
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 (
2026-07-08 01:13:02 -04:00
fixture . view . debugCardRailDocumentHeight ,
fixture . view . debugCardRailVisibleRect . height + 1
2026-06-30 09:02:19 -07:00
)
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardRailVisibleRect . minY , 0 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugCardRailOverflowFadeWidth , 26 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugCardRailOverflowFadeVisibility , [ false , false ] )
2026-06-30 09:02:19 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugScrollCardRailVertically ( deltaY : - 60 )
2026-06-30 09:02:19 -07:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertGreaterThan ( fixture . view . debugCardRailVisibleRect . minY , 0 )
XCTAssertEqual ( fixture . view . debugCardRailOverflowFadeVisibility , [ false , false ] )
2026-06-30 09:02:19 -07:00
fixture . view . debugScrollCardRailVertically ( deltaY : - 10_000 )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
let maxOffset = fixture . view . debugCardRailDocumentHeight - fixture . view . debugCardRailVisibleRect . height
XCTAssertEqual ( fixture . view . debugCardRailVisibleRect . minY , maxOffset , accuracy : 1 )
XCTAssertEqual ( fixture . view . debugCardRailOverflowFadeVisibility , [ false , false ] )
2026-06-30 09:02:19 -07:00
fixture . view . debugScrollCardRailVertically ( deltaY : 10_000 )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardRailVisibleRect . minY , 0 , accuracy : 1 )
XCTAssertEqual ( fixture . view . debugCardRailOverflowFadeVisibility , [ false , false ] )
2026-06-30 09:02:19 -07:00
}
2026-06-30 01:12:19 -07:00
func testFilteredEmptyStateNamesCurrentCollection ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Only text exists " , store : fixture . store ) )
drainMainQueue ( )
fixture . viewModel . sortMode = . images
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . title , " No images yet " )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . detail , " Image clips are saved when the clipboard contains image data. " )
}
2026-07-08 01:13:02 -04:00
func testEmptySearchResultsUseSearchSpecificStatusCopy ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Only text exists " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugSetSearchFieldText ( " missing " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugVisibleCardCount , 0 )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . title , " No matching clips " )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . detail , " Try a broader search or switch filters. " )
}
2026-06-30 10:48:14 -07:00
func testPinnedEmptyStatePointsToPinAction ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Only text exists " , store : fixture . store ) )
drainMainQueue ( )
fixture . viewModel . sortMode = . pinned
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . title , " No pinned clips " )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . detail , " Use the Pin action on a card to keep important clips here. " )
}
2026-07-08 01:13:02 -04:00
func testCardsExposeContextMenuActions ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Context menu text " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardMenuTitles ,
[ " 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 " ]
)
}
2026-07-09 19:32:57 -04:00
func testCaptureRuleActionsPublishUsefulStatusMessages ( ) {
2026-07-08 01:13:02 -04:00
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Capture rule status tone " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugPerformFirstCardCaptureRuleMenuItem ( titled : " Ignore Ghostty " )
drainMainQueue ( )
XCTAssertTrue ( fixture . settings . ignoredApps . contains ( " Ghostty " ) )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Ignored Ghostty for future captures " )
2026-07-08 01:13:02 -04:00
fixture . view . debugPerformFirstCardCaptureRuleMenuItem ( titled : " Ignore Text Items " )
drainMainQueue ( )
XCTAssertTrue ( fixture . settings . ignoredItemKindsRaw . contains ( ClipboardItemKind . text . rawValue ) )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Ignored Text items for future captures " )
2026-07-08 01:13:02 -04:00
}
func testFilteredCardsExposeShowInClipboardContextMenuAction ( ) {
let fixture = makePanelFixture ( )
var release = makeTextItem ( " Release needle " , store : fixture . store )
release . createdAt = Date ( timeIntervalSince1970 : 100 )
release . lastUsedAt = release . createdAt
var meeting = makeTextItem ( " Meeting note " , store : fixture . store )
meeting . createdAt = Date ( timeIntervalSince1970 : 200 )
meeting . lastUsedAt = meeting . createdAt
fixture . store . upsert ( release )
fixture . store . upsert ( meeting )
drainMainQueue ( )
fixture . viewModel . searchText = " release "
2026-06-30 01:12:19 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardMenuTitles ,
2026-07-08 01:13:02 -04:00
[ " Paste " , " Copy " , " Show in Clipboard " , " Rename... " , " Add to Stack " , " Add Visible Clips to Stack " , " Edit " , " Quick Look " , " Pin " , " Add to Collection " , " Capture Rules " , " - " , " Open " , " Reveal in Finder " , " - " , " Delete " ]
2026-06-30 02:49:35 -07:00
)
2026-07-08 01:13:02 -04:00
fixture . view . debugShowFirstCardInClipboard ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Meeting note " , " Release needle " ] )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Release needle " )
2026-06-30 01:12:19 -07:00
}
2026-07-08 01:13:02 -04:00
func testShowInClipboardCollapsesClearedUISearchFieldWhenIdle ( ) {
2026-06-30 03:28:50 -07:00
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 ( )
2026-07-08 01:13:02 -04:00
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 03:28:50 -07:00
2026-07-08 01:13:02 -04:00
fixture . view . debugSetSearchFieldText ( " release " )
2026-06-30 03:28:50 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Release needle " ] )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " release " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-06-30 03:28:50 -07:00
fixture . view . debugShowFirstCardInClipboard ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
2026-06-30 03:28:50 -07:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Meeting note " , " Release needle " ] )
XCTAssertEqual ( fixture . viewModel . selectedItem ? . payload , " Release needle " )
}
2026-07-08 01:13:02 -04:00
func testModelSideSearchClearSynchronizesSearchFieldPresentation ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Launch note " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Meeting note " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . view . debugSetSearchFieldText ( " launch " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " launch " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 240 , accuracy : 0.5 )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Launch note " ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
fixture . viewModel . clearSearch ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSearchFieldText , " " )
XCTAssertEqual ( fixture . view . debugSearchFieldWidth , 30 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugSearchFieldPlaceholderText , " " )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " Meeting note " , " Launch note " ] )
}
2026-06-30 01:48:35 -07:00
func testPreviewableCardsExposeQuickLookContextMenuAction ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeItem ( kind : . file , text : " /tmp/report.txt " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardMenuTitles ,
2026-07-01 15:20:13 -07:00
[ " Paste " , " Copy " , " Paste Plain Text " , " Copy Plain Text " , " Rename... " , " Add to Stack " , " Add Visible Clips to Stack " , " Quick Look " , " Pin " , " Add to Collection " , " Capture Rules " , " - " , " Open " , " Reveal in Finder " , " - " , " Delete " ]
2026-06-30 02:49:35 -07:00
)
XCTAssertEqual (
fixture . view . debugFirstCardCaptureRuleMenuTitles ,
[ " Ignore Ghostty " , " Ignore File Items " ]
2026-06-30 01:48:35 -07:00
)
}
2026-07-08 01:13:02 -04:00
func testImageCardsExposeImageQuickActions ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeCachedImageItem ( store : fixture . store , cacheService : fixture . cacheService ) )
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 " , " Rotate Image " , " Extract Text " , " Quick Look " , " Pin " , " Add to Collection " , " Capture Rules " , " - " , " Open " , " Reveal in Finder " , " - " , " Delete " ]
)
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
}
func testMultiSelectedCardsExposeBatchActionsAndSelectionCount ( ) {
let fixture = makePanelFixture ( )
var oldest = makeTextItem ( " Oldest multi-select note " , store : fixture . store )
oldest . createdAt = Date ( timeIntervalSince1970 : 100 )
oldest . lastUsedAt = oldest . createdAt
var middle = makeTextItem ( " Middle multi-select note " , store : fixture . store )
middle . createdAt = Date ( timeIntervalSince1970 : 200 )
middle . lastUsedAt = middle . createdAt
var newest = makeTextItem ( " Newest multi-select note " , store : fixture . store )
newest . createdAt = Date ( timeIntervalSince1970 : 300 )
newest . lastUsedAt = newest . createdAt
fixture . store . upsert ( oldest )
fixture . store . upsert ( middle )
fixture . store . upsert ( newest )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . viewModel . selectItem ( at : 0 )
fixture . viewModel . selectItem ( at : 2 , mode : . toggle )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedCardIndexes , [ 0 , 2 ] )
XCTAssertEqual ( fixture . view . debugActiveCardIndexes , [ ] )
XCTAssertEqual ( fixture . view . debugCardAccessibilityValues , [ " Selected " , " Not selected " , " Selected " ] )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Paste Selection " ) )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Copy Selection " ) )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Paste Selection as Text " ) )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Copy Selection as Text " ) )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Add Selection to Stack " ) )
fixture . view . debugPerformFirstCardMenuItem ( titled : " Add Selection to Stack " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Added 2 selected clips to Stack " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugStackChipCount , 2 )
}
2026-06-30 02:11:50 -07:00
func testStackedCardsExposeStackManagementActions ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " Stackable text " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . viewModel . toggleSelectedStackMembership ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardMenuTitles ,
2026-07-01 15:58:30 -07:00
[ " 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 " ]
2026-06-30 02:11:50 -07:00
)
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
2026-06-30 02:11:50 -07:00
}
2026-07-09 19:32:57 -04:00
func testStackMembershipUpdatesCardsWithoutRebuildingThem ( ) {
2026-06-30 10:30:07 -07:00
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 ( )
2026-06-30 10:34:37 -07:00
XCTAssertFalse ( fixture . view . debugFirstCardVisibleActionLabels . contains ( " Add to Stack " ) )
2026-07-08 01:13:02 -04:00
let cardIdentifiersBeforeStackToggle = fixture . view . debugCardObjectIdentifiers
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . viewModel . toggleSelectedStackMembership ( )
2026-06-30 10:30:07 -07:00
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Added to Stack " )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCardObjectIdentifiers , cardIdentifiersBeforeStackToggle )
2026-07-09 19:32:57 -04:00
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Remove from Stack " ) )
2026-06-30 10:30:07 -07:00
XCTAssertTrue ( fixture . view . debugStackChipIsVisible )
XCTAssertEqual ( fixture . view . debugStackChipCount , 1 )
fixture . viewModel . selectItem ( at : 1 )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . view . debugCardObjectIdentifiers , cardIdentifiersBeforeStackToggle )
2026-06-30 10:30:07 -07:00
}
2026-06-30 02:19:03 -07:00
func testStackChipAppearsFiltersAndClearsWithStack ( ) {
let fixture = makePanelFixture ( )
fixture . store . upsert ( makeTextItem ( " First stack chip item " , store : fixture . store ) )
fixture . store . upsert ( makeTextItem ( " Second stack chip item " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugStackChipIsVisible )
fixture . viewModel . selectItem ( at : 1 )
fixture . viewModel . toggleSelectedStackMembership ( )
fixture . viewModel . selectItem ( at : 0 )
fixture . viewModel . toggleSelectedStackMembership ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugStackChipIsVisible )
XCTAssertEqual ( fixture . view . debugStackChipCount , 2 )
XCTAssertFalse ( fixture . view . debugStackChipIsSelected )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ " Clipboard " ] )
2026-06-30 02:19:03 -07:00
fixture . view . debugPressStackChip ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Stack " )
XCTAssertTrue ( fixture . view . debugStackChipIsSelected )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugSelectedSortCollectionTitles , [ ] )
2026-06-30 02:19:03 -07:00
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . payload ) , [ " First stack chip item " , " Second stack chip item " ] )
fixture . viewModel . clearStack ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugStackChipIsVisible )
XCTAssertEqual ( fixture . view . debugStackChipCount , 0 )
}
2026-07-08 01:13:02 -04:00
func testStackCaptureModeShowsEmptyActiveStackChip ( ) {
let fixture = makePanelFixture ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . view . debugStackChipIsVisible )
fixture . viewModel . toggleStackCaptureMode ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugStackChipIsVisible )
XCTAssertEqual ( fixture . view . debugStackChipCount , 0 )
XCTAssertEqual ( fixture . view . debugStackChipMenuTitles . first , " Stop Stack Capture " )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Stack capture is on " )
2026-07-08 01:13:02 -04:00
fixture . view . debugPressStackChip ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugSelectedCollectionTitle , " Stack " )
XCTAssertTrue ( fixture . view . debugStackChipIsSelected )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . title , " Stack capture is on " )
XCTAssertEqual ( fixture . view . debugEmptyStateText ? . detail , " Copied items will appear here in order. " )
fixture . view . debugToggleStackCaptureFromStackChip ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertFalse ( fixture . viewModel . isStackCaptureEnabled )
XCTAssertFalse ( fixture . view . debugStackChipIsVisible )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Stack capture is off " )
2026-07-08 01:13:02 -04:00
}
2026-07-01 15:20:13 -07:00
func testStackChipMenuAddsVisibleShelfToQueue ( ) {
let fixture = makePanelFixture ( )
var older = makeTextItem ( " Older batch stack item " , store : fixture . store )
older . createdAt = Date ( timeIntervalSince1970 : 100 )
older . lastUsedAt = older . createdAt
var middle = makeTextItem ( " Middle batch stack item " , store : fixture . store )
middle . createdAt = Date ( timeIntervalSince1970 : 200 )
middle . lastUsedAt = middle . createdAt
var newest = makeTextItem ( " Newest batch stack item " , store : fixture . store )
newest . createdAt = Date ( timeIntervalSince1970 : 300 )
newest . lastUsedAt = newest . createdAt
fixture . store . upsert ( older )
fixture . store . upsert ( middle )
fixture . store . upsert ( newest )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
fixture . viewModel . selectItem ( at : 0 )
fixture . viewModel . toggleSelectedStackMembership ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugStackChipIsVisible )
XCTAssertEqual ( fixture . view . debugStackChipCount , 1 )
XCTAssertEqual (
fixture . view . debugStackChipMenuTitles ,
2026-07-08 01:13:02 -04:00
[ " Start Stack Capture " , " - " , " Add Visible Clips to Stack " , " Paste Stack Next " , " Copy Stack Next " , " Paste Stack as Text " , " Copy Stack as Text " , " Clear Stack " ]
)
XCTAssertEqual (
fixture . view . debugStackChipAccessibilityHelp ,
2026-07-09 19:32:57 -04:00
" Press Return or Space to show Stack; Command-Return or Command-Space combines categories. Use Left/Right to move between categories, Up/Down to move between clips, and Home/End to jump between categories. Open the context menu for Stack capture and Stack paste actions. "
2026-07-01 15:20:13 -07:00
)
fixture . view . debugAddVisibleClipsToStackFromStackChip ( )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugStackChipCount , 3 )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Added 2 clips to Stack " )
2026-07-01 15:20:13 -07:00
fixture . view . debugPressStackChip ( )
drainMainQueue ( )
XCTAssertEqual (
fixture . viewModel . visibleItems . map ( \ . payload ) ,
[ " Newest batch stack item " , " Middle batch stack item " , " Older batch stack item " ]
)
}
2026-06-30 01:12:19 -07:00
func testCollectionMenuOffersExistingCustomCollections ( ) {
let fixture = makePanelFixture ( )
var existing = makeTextItem ( " Existing client note " , store : fixture . store )
existing . collectionName = " Client Work "
fixture . store . upsert ( existing )
fixture . store . upsert ( makeTextItem ( " Unsorted card " , store : fixture . store ) )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual (
fixture . view . debugFirstCardCollectionMenuTitles ,
[ " Useful Links " , " Important Notes " , " Code Snippets " , " Read Later " , " Client Work " , " - " , " New Collection... " ]
)
2026-06-30 03:17:40 -07:00
XCTAssertEqual (
fixture . view . debugFirstCardCollectActionMenuTitles ,
[ " Useful Links " , " Important Notes " , " Code Snippets " , " Read Later " , " Client Work " , " - " , " New Collection... " ]
)
2026-06-30 01:12:19 -07:00
}
func testBottomSafeInsetIsAppliedToPanelContent ( ) {
let fixture = makePanelFixture ( )
fixture . view . setBottomSafeInset ( 108 )
XCTAssertEqual ( fixture . view . debugContentInsets . bottom , 108 )
}
func testInternalLookingTextDoesNotBecomePrimaryCardTitle ( ) {
let fixture = makePanelFixture ( )
let item = makeTextItem ( " clipbored-flow-test- \( UUID ( ) . uuidString ) " , store : fixture . store )
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Text: Copied text " ] )
}
func testLinkCardsUseReadableTitleAndAddressPreview ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . url ,
displayText : " Release notes " ,
payload : " https://www.example.com/releases/v1?utm_source=copy " ,
store : fixture . store
)
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Link: Release notes " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Release notes|example.com/releases/v1|example.com " ] )
2026-07-01 14:59:52 -07:00
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " link-site-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewChrome , " browser-card " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewHostText , " example.com " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewMonogram , " EC " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewAccentHex , " #008FFB " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewHeroHeight , 78 , accuracy : 0.5 )
2026-07-01 14:59:52 -07:00
}
func testPlainURLCardsDeriveReadableTitleFromPath ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . url ,
displayText : " https://www.example.com/articles/weekly-design-review?utm_source=copy " ,
payload : " https://www.example.com/articles/weekly-design-review?utm_source=copy " ,
store : fixture . store
)
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Link: Weekly Design Review " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Weekly Design Review|example.com/articles/weekly-design-review|example.com " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " link-site-preview " ] )
2026-06-30 01:12:19 -07:00
}
2026-07-08 01:13:02 -04:00
func testCompactLinkCardUsesCondensedBrowserPreview ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
let item = makeItem (
kind : . url ,
displayText : " Weekly Design Review " ,
payload : " https://www.example.com/articles/weekly-design-review " ,
store : fixture . store
)
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardDensity , " compact " )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " link-site-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewChrome , " browser-card " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewHostText , " example.com " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkPreviewHeroHeight , 78 , accuracy : 0.5 )
}
2026-06-30 01:12:19 -07:00
func testLinkCardsUseMediaPreviewWhenThumbnailExists ( ) throws {
let fixture = makePanelFixture ( )
let id = UUID ( )
let paths = try XCTUnwrap ( fixture . cacheService . cacheImage ( sampleImage ( ) , id : id ) )
let item = ClipboardItem (
id : id ,
kind : . url ,
displayText : " Lookbook " ,
payload : " https://example.com/lookbook " ,
payloadHash : fixture . store . hashString ( " https://example.com/lookbook " ) ,
createdAt : Date ( ) ,
lastUsedAt : Date ( ) ,
useCount : 0 ,
sourceApp : " Safari " ,
imagePath : paths . full ,
thumbnailPath : paths . thumb
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . links
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Link: Lookbook " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Lookbook|example.com/lookbook|example.com " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " link-media-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardLinkMediaPreviewChrome , " thumbnail-card " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkMediaPreviewHostText , " example.com " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkMediaPreviewImageHeight , 80 , accuracy : 0.5 )
}
func testTallSideShelfKeepsLinkMediaCardCompact ( ) throws {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 1280 , height : 640 ) , display : true )
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 . debugCardDensity , " compact " )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " link-media-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardLinkMediaPreviewChrome , " thumbnail-card " )
XCTAssertEqual ( fixture . view . debugFirstCardLinkMediaPreviewImageHeight , 80 , accuracy : 0.5 )
2026-06-30 01:12:19 -07:00
}
func testRichTextCardsUseDisplayTextInsteadOfManagedPayloadPath ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . richText ,
displayText : " Styled note " ,
payload : " /tmp/clipbored-managed-rich-text.rtf " ,
store : fixture . store
)
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Rich Text: Styled note " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Styled note|Styled note|11 characters " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " rich-text-preview " ] )
}
func testFileCardsUseFilenameLocationAndType ( ) {
let fixture = makePanelFixture ( )
let fileURL = FileManager . default . homeDirectoryForCurrentUser
. appendingPathComponent ( " Documents " )
. appendingPathComponent ( " Project Plan.pdf " )
let item = makeItem ( kind : . file , displayText : " File " , payload : fileURL . path , store : fixture . store )
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " File: Project Plan.pdf " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Project Plan.pdf|~/Documents|PDF " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " file-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewChrome , " document-cover " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewExtensionText , " PDF " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewLocationPlacement , " below-cover " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewCoverSize , NSSize ( width : 104 , height : 74 ) )
}
func testCompactFileCardsUseCondensedDocumentCover ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
let fileURL = FileManager . default . homeDirectoryForCurrentUser
. appendingPathComponent ( " Desktop " )
. appendingPathComponent ( " Summary.txt " )
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 . debugCardPreviewStyles , [ " file-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewChrome , " document-cover " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewExtensionText , " TXT " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewCoverSize , NSSize ( width : 104 , height : 74 ) )
2026-06-30 01:12:19 -07:00
}
2026-06-30 04:10:12 -07:00
func testRenamedClipsUseCustomTitleInCardsAndSearch ( ) {
let fixture = makePanelFixture ( )
let fileURL = FileManager . default . homeDirectoryForCurrentUser
. appendingPathComponent ( " Documents " )
. appendingPathComponent ( " Project Plan.pdf " )
let item = makeItem ( kind : . file , displayText : " File " , payload : fileURL . path , store : fixture . store )
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertTrue ( fixture . view . debugFirstCardMenuTitles . contains ( " Rename... " ) )
fixture . view . debugRenameFirstCard ( to : " Client Launch Brief " )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " File: Client Launch Brief " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Client Launch Brief|~/Documents|PDF " ] )
2026-07-09 19:32:57 -04:00
XCTAssertEqual ( fixture . viewModel . statusMessage , " Renamed clip " )
2026-06-30 04:10:12 -07:00
fixture . viewModel . searchText = " launch "
drainMainQueue ( )
XCTAssertEqual ( fixture . viewModel . visibleItems . map ( \ . id ) , [ item . id ] )
}
2026-06-30 01:12:19 -07:00
func testMultipleFileCardsUseCountAndSharedLocation ( ) throws {
let fixture = makePanelFixture ( )
let directory = makeTempDirectory ( )
let firstURL = directory . appendingPathComponent ( " Brief.pdf " )
let secondURL = directory . appendingPathComponent ( " Invoice.csv " )
try Data ( " brief " . utf8 ) . write ( to : firstURL )
try Data ( " invoice " . utf8 ) . write ( to : secondURL )
let item = makeItem (
kind : . file ,
displayText : " 2 files " ,
payload : FilePayload . payload ( from : [ firstURL , secondURL ] ) ,
store : fixture . store
)
fixture . store . upsert ( item )
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " File: 2 files " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " 2 files| \( directory . path ) |2 files " ] )
2026-07-01 14:33:04 -07:00
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " multi-file-preview " ] )
2026-06-30 01:12:19 -07:00
}
func testExistingFileCardsUseFullBleedMediaPreviewLayout ( ) throws {
let fixture = makePanelFixture ( )
let imageURL = makeTempDirectory ( ) . appendingPathComponent ( " Campaign Reference.png " )
let imageData = try XCTUnwrap ( sampleImage ( ) . pngData ( ) )
try imageData . write ( to : imageURL )
let item = makeItem ( kind : . file , displayText : " File " , payload : imageURL . path , store : fixture . store )
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . files
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " File: Campaign Reference.png " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " file-media-preview " ] )
}
func testPdfAndImageCardsUseSpecificPreviewText ( ) {
let fixture = makePanelFixture ( )
let pdfURL = FileManager . default . homeDirectoryForCurrentUser
. appendingPathComponent ( " Downloads " )
. appendingPathComponent ( " Reference Guide.pdf " )
let pdf = makeItem (
kind : . pdf ,
displayText : " PDF " ,
payload : pdfURL . path ,
store : fixture . store ,
ocrText : " Quarterly metrics \n Second 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 " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewChrome , " document-cover " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewExtensionText , " PDF " )
XCTAssertEqual ( fixture . view . debugFirstCardFilePreviewLocationPlacement , " below-cover " )
2026-06-30 01:12:19 -07:00
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 " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardMediaMetricText , " 360 x 280 " )
XCTAssertEqual ( fixture . view . debugFirstCardMediaMetricPlacement , " bottom-center " )
2026-06-30 01:12:19 -07:00
}
func testAudioCardsUseSpecificPreviewText ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . audio ,
displayText : " Audio (14 KB) " ,
payload : " /tmp/clipbored-audio.sound " ,
store : fixture . store
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . audio
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Audio: Audio (14 KB) " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Audio (14 KB)|Sound clip|Audio " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " audio-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardAudioPreviewChrome , " album-card " )
XCTAssertEqual ( fixture . view . debugFirstCardAudioArtworkSize , 62 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugFirstCardAudioLabelPlacement , " below-artwork " )
}
func testCompactAudioCardsUseCondensedAlbumArtwork ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
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 . debugCardDensity , " compact " )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " audio-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardAudioPreviewChrome , " album-card " )
XCTAssertEqual ( fixture . view . debugFirstCardAudioArtworkSize , 62 , accuracy : 0.5 )
2026-06-30 01:12:19 -07:00
}
2026-07-01 15:39:43 -07:00
func testVideoCardsUseFilmPreview ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . video ,
displayText : " Video (24 KB) " ,
payload : " /tmp/clipbored-video.mp4 " ,
store : fixture . store
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . videos
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Video: Video (24 KB) " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " Video (24 KB)|Video clip|MP4 " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " video-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardVideoPreviewChrome , " player-card " )
XCTAssertEqual ( fixture . view . debugFirstCardVideoFrameHeight , 82 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugFirstCardVideoFormatPlacement , " bottom-center " )
}
func testCompactVideoCardsUseCondensedPlayerPreview ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
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 . debugCardDensity , " compact " )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " video-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardVideoPreviewChrome , " player-card " )
XCTAssertEqual ( fixture . view . debugFirstCardVideoFrameHeight , 82 , accuracy : 0.5 )
XCTAssertEqual ( fixture . view . debugFirstCardVideoFormatPlacement , " bottom-center " )
2026-07-01 15:39:43 -07:00
}
2026-07-01 15:47:45 -07:00
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 " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardVideoPreviewChrome , " thumbnail-player " )
XCTAssertEqual ( fixture . view . debugFirstCardVideoFormatPlacement , " bottom-center " )
2026-07-01 15:47:45 -07:00
}
2026-07-01 14:52:54 -07:00
func testColorCardsUseSwatchPreview ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . color ,
displayText : " #0A84FF " ,
payload : " #0A84FF " ,
store : fixture . store
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . colors
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Color: #0A84FF " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewSummaries , [ " #0A84FF|RGB 10 132 255|Color " ] )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " color-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewChrome , " paint-chip " )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewHexText , " #0A84FF " )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewSwatchPlacement , " top " )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewChipSize , NSSize ( width : 196 , height : 98 ) )
}
func testCompactColorCardsUseCondensedPaintChipPreview ( ) {
let fixture = makePanelFixture ( )
fixture . window . setFrame ( NSRect ( x : 0 , y : 0 , width : 620 , height : 520 ) , display : true )
let item = makeItem (
kind : . color ,
displayText : " #FF3B30 " ,
payload : " #FF3B30 " ,
store : fixture . store
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . colors
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " color-preview " ] )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewChrome , " paint-chip " )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewHexText , " #FF3B30 " )
XCTAssertEqual ( fixture . view . debugFirstCardColorPreviewChipSize , NSSize ( width : 196 , height : 98 ) )
2026-07-01 14:52:54 -07:00
}
2026-07-01 15:12:49 -07:00
func testCodeCardsUseMonospaceSnippetPreview ( ) {
let fixture = makePanelFixture ( )
let item = makeItem (
kind : . code ,
displayText : " Swift Snippet " ,
payload : " func greet(name: String) -> String { \n return \" Hi \\ (name) \" \n } " ,
store : fixture . store
)
fixture . store . upsert ( item )
fixture . viewModel . sortMode = . code
drainMainQueue ( )
fixture . window . contentView ? . layoutSubtreeIfNeeded ( )
XCTAssertEqual ( fixture . view . debugCardAccessibilityLabels , [ " Code: Swift Snippet " ] )
XCTAssertEqual (
fixture . view . debugCardPreviewSummaries ,
[ " Swift Snippet|func greet(name: String) -> String { return \" Hi \\ (name) \" }|Swift " ]
)
XCTAssertEqual ( fixture . view . debugCardPreviewStyles , [ " code-preview " ] )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
XCTAssertTrue ( fixture . view . debugFocusCard ( at : 0 ) )
drainMainQueue ( )
XCTAssertEqual ( fixture . view . debugFirstCardVisibleActionLabels , [ ] )
}
private func assertCollectionRailIsSideIconRail ( in fixture : PanelFixture , file : StaticString = # filePath , line : UInt = #line ) {
XCTAssertEqual ( fixture . view . debugCollectionRailVisibleWidth , 36 , accuracy : 0.5 , file : file , line : line )
2026-07-09 19:32:57 -04:00
XCTAssertGreaterThan ( fixture . view . debugCollectionRailHeight , 200 , file : file , line : line )
2026-07-08 01:13:02 -04:00
XCTAssertEqual ( fixture . view . debugCollectionStackOrientation , . vertical , file : file , line : line )
2026-07-09 19:32:57 -04:00
XCTAssertLessThanOrEqual (
fixture . view . debugCollectionRailFrameInPanel . maxX + 8 ,
fixture . view . debugSelectedCardFrameInPanel . minX + 0.5 ,
2026-07-08 01:13:02 -04:00
file : file ,
line : line
)
}
2026-06-30 01:12:19 -07:00
private func makePanelWithPanelView ( ) -> ( NSWindow , ClipboardPanelView ) {
let fixture = makePanelFixture ( )
return ( fixture . window , fixture . view )
}
2026-07-01 15:47:45 -07:00
private func makePanelFixture ( cacheService : ClipboardCacheService ? = nil ) -> PanelFixture {
2026-06-30 01:12:19 -07:00
let settings = makeSettings ( )
2026-07-08 01:13:02 -04:00
let cacheService = cacheService ? ? ClipboardCacheService (
baseURL : makeTempDirectory ( ) ,
encryptionService : ClipboardEncryptionService ( keyProvider : { nil } )
)
2026-06-30 01:12:19 -07:00
let store = makeStore ( settings : settings , cacheService : cacheService )
let viewModel = ClipboardPanelViewModel ( store : store , settings : settings , cacheService : cacheService )
2026-06-30 08:56:45 -07:00
let previewProbe = PreviewProbe ( )
2026-06-30 01:12:19 -07:00
let view = ClipboardPanelView (
viewModel : viewModel ,
onClose : { } ,
2026-06-30 08:56:45 -07:00
onSettings : { } ,
onPreview : { previewProbe . requestCount += 1 }
2026-06-30 01:12:19 -07:00
)
let window = NSPanel (
contentRect : NSRect ( x : 0 , y : 0 , width : 1200 , height : 520 ) ,
styleMask : [ . titled , . closable ] ,
backing : . buffered ,
defer : false
)
window . contentView = view
window . makeKeyAndOrderFront ( nil )
return PanelFixture (
window : window ,
view : view ,
viewModel : viewModel ,
settings : settings ,
store : store ,
2026-06-30 08:56:45 -07:00
cacheService : cacheService ,
previewProbe : previewProbe
2026-06-30 01:12:19 -07:00
)
}
private func makeSettings ( ) -> SettingsModel {
let settings = SettingsModel ( defaults : UserDefaults ( suiteName : " com.clipbored.viewtest. \( UUID ( ) . uuidString ) " ) ! )
settings . maxHistoryItems = 10
2026-07-08 01:13:02 -04:00
settings . historyRetention = . forever
2026-06-30 01:12:19 -07:00
settings . pruneDuplicates = false
return settings
}
private func makeStore ( settings : SettingsModel , cacheService : ClipboardCacheService ) -> ClipboardStore {
2026-07-08 01:13:02 -04:00
ClipboardStore (
settings : settings ,
cacheService : cacheService ,
baseURL : makeTempDirectory ( ) ,
encryptionService : ClipboardEncryptionService ( keyProvider : { nil } )
)
2026-06-30 01:12:19 -07:00
}
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
}
2026-07-08 01:13:02 -04:00
private func localDate ( _ value : String , hour : Int = 12 , minute : Int = 0 ) -> Date {
var calendar = Calendar ( identifier : . gregorian )
calendar . locale = Locale ( identifier : " en_US_POSIX " )
calendar . timeZone = . autoupdatingCurrent
let formatter = DateFormatter ( )
formatter . calendar = calendar
formatter . locale = Locale ( identifier : " en_US_POSIX " )
formatter . timeZone = calendar . timeZone
formatter . dateFormat = " yyyy-MM-dd "
let date = formatter . date ( from : value ) !
return calendar . date ( bySettingHour : hour , minute : minute , second : 0 , of : date ) !
}
private func writeSnapshot ( of view : NSView , to url : URL ) throws {
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 image = NSImage ( size : view . bounds . size )
image . addRepresentation ( rep )
let tiff = try XCTUnwrap ( image . tiffRepresentation )
let bitmap = try XCTUnwrap ( NSBitmapImageRep ( data : tiff ) )
let png = try XCTUnwrap ( bitmap . representation ( using : . png , properties : [ : ] ) )
try png . write ( to : url )
}
2026-06-30 01:12:19 -07:00
private func drainMainQueue ( ) {
for _ in 0. . < 20 {
RunLoop . main . run ( until : Date ( ) . addingTimeInterval ( 0.01 ) )
}
}
}