UI fixes
Some checks failed
CI / Test And Build (push) Has been cancelled

This commit is contained in:
Akshay Kolli
2026-07-08 01:13:02 -04:00
parent c7316105c7
commit d86d392c3a
46 changed files with 17543 additions and 1561 deletions

View File

@@ -78,9 +78,14 @@ final class AppDelegateTests: XCTestCase {
"Captured text from Safari.",
"-",
"Show Clipboard",
"New Collection",
"Stack Capture",
settingsTitle,
"-",
"Pause Capture",
"Pause for 5 Minutes",
"Pause for 30 Minutes",
"Pause for 1 Hour",
"-",
"Quit ClipBored"
]
@@ -91,9 +96,25 @@ final class AppDelegateTests: XCTestCase {
XCTAssertTrue(showClipboard?.keyEquivalentModifierMask.contains(.command) == true)
XCTAssertTrue(showClipboard?.keyEquivalentModifierMask.contains(.option) == true)
XCTAssertNil(menu.items.first { $0.title == "New Text Clip" })
let newCollection = menu.items.first { $0.title == "New Collection" }
XCTAssertEqual(newCollection?.keyEquivalent, "n")
XCTAssertTrue(newCollection?.keyEquivalentModifierMask.contains(.command) == true)
XCTAssertTrue(newCollection?.keyEquivalentModifierMask.contains(.shift) == true)
let stackCapture = menu.items.first { $0.title == "Stack Capture" }
XCTAssertEqual(stackCapture?.keyEquivalent, "c")
XCTAssertTrue(stackCapture?.keyEquivalentModifierMask.contains(.command) == true)
XCTAssertTrue(stackCapture?.keyEquivalentModifierMask.contains(.shift) == true)
let settings = menu.items.first { $0.title == settingsTitle }
XCTAssertEqual(settings?.keyEquivalent, ",")
XCTAssertTrue(settings?.keyEquivalentModifierMask.contains(.command) == true)
let pauseCapture = menu.items.first { $0.title == "Pause Capture" }
XCTAssertEqual(pauseCapture?.keyEquivalent, "t")
XCTAssertTrue(pauseCapture?.keyEquivalentModifierMask.contains(.command) == true)
}
func testStatusMenuPausedStateTakesPriorityOverOlderCaptureStatus() {
@@ -116,8 +137,55 @@ final class AppDelegateTests: XCTestCase {
XCTAssertEqual(presentation.summary, "Capture Paused - 1 clip")
XCTAssertEqual(presentation.detail, "Capture is paused.")
XCTAssertEqual(menu.items.first { $0.title == "Resume Capture" }?.state, .on)
let resumeCapture = menu.items.first { $0.title == "Resume Capture" }
XCTAssertEqual(resumeCapture?.state, .on)
XCTAssertEqual(resumeCapture?.keyEquivalent, "t")
XCTAssertTrue(resumeCapture?.keyEquivalentModifierMask.contains(.command) == true)
XCTAssertNil(menu.items.first { $0.title == "Pause Capture" })
XCTAssertNil(menu.items.first { $0.title == "Pause for 5 Minutes" })
}
func testStatusMenuPresentationShowsTimedPauseRemainingBeforeOlderStatuses() {
let now = Date(timeIntervalSince1970: 1_000)
let presentation = AppDelegate.statusMenuPresentation(
historyCount: 12,
isCapturePaused: true,
pauseCaptureUntil: now.addingTimeInterval(5 * 60),
now: now,
captureStatus: "Captured text from Safari.",
pasteStatus: "",
shortcutStatus: "",
accessibilityStatus: "",
launchAtLoginStatus: ""
)
XCTAssertEqual(presentation.summary, "Capture Paused - 12 clips")
XCTAssertEqual(presentation.detail, "Capture is paused for 5 more minutes.")
}
func testCapturePauseExpiryOnlyAppliesToExpiredTimedPauses() {
let now = Date(timeIntervalSince1970: 1_000)
XCTAssertTrue(AppDelegate.shouldResumeExpiredCapturePause(
isCapturePaused: true,
pauseCaptureUntil: now,
now: now
))
XCTAssertFalse(AppDelegate.shouldResumeExpiredCapturePause(
isCapturePaused: true,
pauseCaptureUntil: now.addingTimeInterval(1),
now: now
))
XCTAssertFalse(AppDelegate.shouldResumeExpiredCapturePause(
isCapturePaused: false,
pauseCaptureUntil: now.addingTimeInterval(-1),
now: now
))
XCTAssertFalse(AppDelegate.shouldResumeExpiredCapturePause(
isCapturePaused: true,
pauseCaptureUntil: nil,
now: now
))
}
func testStatusMenuPresentationTruncatesLongStatusText() {

View File

@@ -0,0 +1,144 @@
import Foundation
import XCTest
@testable import ClipBored
final class ClipboardCloudSyncServiceTests: XCTestCase {
private var tempRoot: URL!
private var defaultsSuites: [String] = []
override func setUpWithError() throws {
try super.setUpWithError()
tempRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("clipbored-cloud-sync-tests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempRoot, withIntermediateDirectories: true)
}
override func tearDownWithError() throws {
for suite in defaultsSuites {
UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite)
}
if let tempRoot {
try? FileManager.default.removeItem(at: tempRoot)
}
defaultsSuites = []
tempRoot = nil
try super.tearDownWithError()
}
func testUnavailableContainerReportsStatusAndThrows() {
let service = ClipboardCloudSyncService(containerProvider: { nil })
let status = service.status()
XCTAssertFalse(status.isAvailable)
XCTAssertNil(status.archiveURL)
XCTAssertTrue(status.message.contains("iCloud Sync is unavailable"))
XCTAssertThrowsError(try service.syncArchiveURL()) { error in
XCTAssertEqual(error as? ClipboardCloudSyncError, .unavailable)
}
}
func testPushWritesArchiveToPrivateDocumentsFolder() throws {
let environment = try makeStoreEnvironment(named: "source")
environment.store.upsert(makeItem("cloud note", created: Date(timeIntervalSince1970: 10)))
environment.store.flushPersistenceForTesting()
let cloudRoot = tempRoot.appendingPathComponent("cloud", isDirectory: true)
let service = ClipboardCloudSyncService(containerProvider: { cloudRoot })
let summary = try service.push(store: environment.store)
let archiveURL = try service.syncArchiveURL()
let status = service.status()
XCTAssertEqual(summary.itemCount, 1)
XCTAssertEqual(
archiveURL,
cloudRoot
.appendingPathComponent("Documents", isDirectory: true)
.appendingPathComponent(AppConfiguration.appName, isDirectory: true)
.appendingPathComponent(ClipboardCloudSyncService.archiveFileName)
)
XCTAssertTrue(FileManager.default.fileExists(atPath: archiveURL.path))
XCTAssertEqual(try posixPermissions(archiveURL), 0o600)
XCTAssertTrue(status.isAvailable)
XCTAssertEqual(status.archiveURL, archiveURL)
XCTAssertNotNil(status.lastModifiedAt)
}
func testPullImportsExistingCloudArchiveIntoAnotherStore() throws {
let source = try makeStoreEnvironment(named: "source")
let sourceItem = makeItem("shared through icloud", created: Date(timeIntervalSince1970: 20))
source.store.upsert(sourceItem)
source.store.flushPersistenceForTesting()
let cloudRoot = tempRoot.appendingPathComponent("cloud", isDirectory: true)
let service = ClipboardCloudSyncService(containerProvider: { cloudRoot })
try service.push(store: source.store)
let destination = try makeStoreEnvironment(named: "destination")
let summary = try service.pull(store: destination.store)
destination.store.flushPersistenceForTesting()
XCTAssertEqual(summary.itemCount, 1)
XCTAssertEqual(destination.store.items.count, 1)
XCTAssertEqual(destination.store.items.first?.id, sourceItem.id)
XCTAssertEqual(destination.store.items.first?.payload, "shared through icloud")
}
func testPullWithoutRemoteArchiveThrowsNoRemoteArchive() throws {
let destination = try makeStoreEnvironment(named: "destination")
let cloudRoot = tempRoot.appendingPathComponent("empty-cloud", isDirectory: true)
let service = ClipboardCloudSyncService(containerProvider: { cloudRoot })
XCTAssertThrowsError(try service.pull(store: destination.store)) { error in
guard case ClipboardCloudSyncError.noRemoteArchive(let url) = error else {
return XCTFail("Expected noRemoteArchive, got \(error)")
}
XCTAssertEqual(url.lastPathComponent, ClipboardCloudSyncService.archiveFileName)
}
}
private func makeStoreEnvironment(named name: String) throws -> (settings: SettingsModel, store: ClipboardStore) {
let suiteName = "com.clipbored.cloudsync.\(name).\(UUID().uuidString)"
defaultsSuites.append(suiteName)
let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
defaults.removePersistentDomain(forName: suiteName)
let settings = SettingsModel(defaults: defaults)
settings.maxHistoryItems = 50
settings.historyRetention = .forever
let baseURL = tempRoot.appendingPathComponent(name, isDirectory: true)
let encryptionService = ClipboardEncryptionService(keyProvider: { nil })
let cacheService = ClipboardCacheService(baseURL: baseURL, encryptionService: encryptionService)
let store = ClipboardStore(
settings: settings,
cacheService: cacheService,
baseURL: baseURL,
encryptionService: encryptionService
)
return (settings, store)
}
private func makeItem(_ payload: String, created: Date) -> ClipboardItem {
ClipboardItem(
id: UUID(),
kind: .text,
displayText: payload,
payload: payload,
payloadHash: String(payload.hashValue),
createdAt: created,
lastUsedAt: created,
useCount: 1,
sourceApp: nil,
imagePath: nil,
thumbnailPath: nil,
isPinned: false
)
}
private func posixPermissions(_ url: URL) throws -> Int {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
return try XCTUnwrap(attributes[.posixPermissions] as? Int)
}
}

View File

@@ -66,6 +66,25 @@ final class ClipboardEncryptionServiceTests: XCTestCase {
XCTAssertEqual(service.protectData(Data("available only in memory".utf8)), Data("available only in memory".utf8))
}
func testSystemKeychainBypassIsEnabledForTestsAndExplicitEnvironment() {
XCTAssertTrue(ClipboardEncryptionService.shouldBypassSystemKeychain(
environment: ["XCTestConfigurationFilePath": "/tmp/ClipBoredTests.xctestconfiguration"],
arguments: ["/tmp/ClipBoredTests"]
))
XCTAssertTrue(ClipboardEncryptionService.shouldBypassSystemKeychain(
environment: ["CLIPBORED_DISABLE_KEYCHAIN": "1"],
arguments: ["/tmp/ClipBored"]
))
XCTAssertTrue(ClipboardEncryptionService.shouldBypassSystemKeychain(
environment: [:],
arguments: ["/tmp/ClipBoredPackageTests.xctest/Contents/MacOS/ClipBoredPackageTests"]
))
XCTAssertFalse(ClipboardEncryptionService.shouldBypassSystemKeychain(
environment: [:],
arguments: ["/Applications/ClipBored.app/Contents/MacOS/ClipBored"]
))
}
private func makeService(byte: UInt8) -> ClipboardEncryptionService {
let keyData = Data(repeating: byte, count: 32)
return ClipboardEncryptionService(keyProvider: { SymmetricKey(data: keyData) })

View File

@@ -20,10 +20,11 @@ final class ClipboardMonitorServiceTests: XCTestCase {
func testClampedIntervalEnforcesResponsiveMinimum() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pollProfile = .responsive
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: makeStore(settings: settings),
cacheService: ClipboardCacheService(),
store: store,
cacheService: cacheService,
settings: settings
)
@@ -36,10 +37,11 @@ final class ClipboardMonitorServiceTests: XCTestCase {
func testClampedIntervalDoesNotIncreaseBalancedProfileWindow() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pollProfile = .balanced
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: makeStore(settings: settings),
cacheService: ClipboardCacheService(),
store: store,
cacheService: cacheService,
settings: settings
)
@@ -108,6 +110,50 @@ final class ClipboardMonitorServiceTests: XCTestCase {
XCTAssertEqual(store.items.filter { $0.payload == text }.count, 1)
}
func testPollNowReportsStoredCapturedItemForStackCapture() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pruneDuplicates = true
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: store,
cacheService: cacheService,
settings: settings
)
let text = "Stack capture duplicate merge \(UUID().uuidString)"
let existing = ClipboardItem(
id: UUID(),
kind: .text,
displayText: text,
payload: text,
payloadHash: store.hashString(text),
createdAt: Date(timeIntervalSince1970: 100),
lastUsedAt: Date(timeIntervalSince1970: 100),
useCount: 0,
sourceApp: nil,
imagePath: nil,
thumbnailPath: nil
)
store.upsert(existing)
let reported = expectation(description: "stored captured item reported")
var reportedItem: ClipboardItem?
monitor.onCapturedItem = { item in
guard item.payload == text else { return }
reportedItem = item
reported.fulfill()
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
wait(for: [reported], timeout: 1.0)
XCTAssertEqual(reportedItem?.id, existing.id)
XCTAssertEqual(store.items.filter { $0.payload == text }.count, 1)
}
func testPollNowCapturesCodeSnippetAsCode() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pruneDuplicates = false
@@ -157,7 +203,7 @@ final class ClipboardMonitorServiceTests: XCTestCase {
thumbnailPath: nil
)
XCTAssertEqual(PasteActionService().copy(item), .copied)
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
@@ -798,7 +844,9 @@ final class ClipboardMonitorServiceTests: XCTestCase {
private func makeTestDefaults() -> UserDefaults {
let suiteName = "com.clipbored.testmonitor.\(UUID().uuidString)"
suiteNames.append(suiteName)
return UserDefaults(suiteName: suiteName)!
let defaults = UserDefaults(suiteName: suiteName)!
defaults.set(HistoryRetention.forever.rawValue, forKey: SettingsModel.Keys.historyRetention)
return defaults
}
private func makeStore(settings: SettingsModel) -> ClipboardStore {

View File

@@ -1,16 +1,80 @@
import AppKit
import XCTest
@testable import ClipBored
final class ClipboardPanelControllerTests: XCTestCase {
func testPanelFrameUsesFullWidthBottomShelf() {
private var tempURLs: [URL] = []
private var defaultsSuites: [String] = []
override func tearDown() {
for url in tempURLs {
try? FileManager.default.removeItem(at: url)
}
tempURLs.removeAll()
for suite in defaultsSuites {
UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite)
}
defaultsSuites.removeAll()
super.tearDown()
}
func testPanelFrameUsesRightSideShelfByDefault() {
let screenFrame = CGRect(x: -1200, y: -200, width: 1200, height: 800)
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: screenFrame)
XCTAssertEqual(frames.shown.minX, screenFrame.minX)
XCTAssertEqual(frames.shown.maxX, screenFrame.maxX)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.height, 408)
XCTAssertLessThan(frames.hidden.maxY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, screenFrame.maxY)
XCTAssertEqual(frames.hidden.minX, screenFrame.maxX + 1)
XCTAssertEqual(frames.hidden.minY, frames.shown.minY)
}
func testOpenScreenSelectionUsesStatusClickScreenWhenProvided() {
XCTAssertEqual(
ClipboardPanelController.selectedOpenScreen(
explicit: "status-item-screen",
preferred: nil,
pointer: "pointer-screen",
fallback: "fallback-screen"
),
"status-item-screen"
)
}
func testOpenScreenSelectionFallsBackToPointerForGlobalShortcut() {
XCTAssertEqual(
ClipboardPanelController.selectedOpenScreen(
explicit: Optional<String>.none,
preferred: nil,
pointer: "pointer-screen",
fallback: "fallback-screen"
),
"pointer-screen"
)
}
func testReflowScreenSelectionKeepsCurrentPanelScreenAheadOfPointer() {
XCTAssertEqual(
ClipboardPanelController.selectedReflowScreen(
currentPanel: "current-panel-screen",
lastKnown: "last-known-screen",
preferred: nil,
pointer: "pointer-screen",
fallback: "fallback-screen"
),
"current-panel-screen"
)
XCTAssertEqual(
ClipboardPanelController.selectedReflowScreen(
currentPanel: Optional<String>.none,
lastKnown: "last-known-screen",
preferred: nil,
pointer: "pointer-screen",
fallback: "fallback-screen"
),
"last-known-screen"
)
}
func testPanelFrameUsesVisibleFrameAroundDock() {
@@ -19,33 +83,48 @@ final class ClipboardPanelControllerTests: XCTestCase {
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(frames.shown.minX, visibleFrame.minX)
XCTAssertEqual(frames.shown.maxX, visibleFrame.maxX)
XCTAssertEqual(frames.shown.minY, visibleFrame.minY)
XCTAssertEqual(frames.shown.height, 408)
XCTAssertEqual(frames.shown.minX, visibleFrame.maxX - 336)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.width, 336)
}
func testPanelFrameSitsAboveVisibleBottomDock() {
func testPanelFrameUsesAvailableHeightWhenBottomDockIsVisible() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 96, width: 1512, height: 861)
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(frames.shown.minX, visibleFrame.minX)
XCTAssertEqual(frames.shown.maxX, visibleFrame.maxX)
XCTAssertEqual(frames.shown.minY, visibleFrame.minY)
XCTAssertEqual(frames.shown.height, 408)
XCTAssertLessThan(frames.hidden.maxY, visibleFrame.minY)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.hidden.minX, visibleFrame.maxX + 1)
}
func testPanelFrameClampsTallDisplaysToShelfMaximum() {
func testPanelFrameTouchesScreenBottomWhenBottomDockIsAutoHidden() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 4, width: 1512, height: 953)
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(frames.shown.maxX, visibleFrame.maxX)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.hidden.minX, visibleFrame.maxX + 1)
}
func testPanelFrameKeepsSideShelfWidthOnTallDisplays() {
let screenFrame = CGRect(x: 0, y: 0, width: 3008, height: 2000)
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: screenFrame)
XCTAssertEqual(frames.shown.width, 3008)
XCTAssertEqual(frames.shown.height, 430)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.shown.height, 2000)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxX, screenFrame.maxX)
}
func testPanelFrameFitsTinyVisibleFrameWithoutOverflowing() {
@@ -54,9 +133,64 @@ final class ClipboardPanelControllerTests: XCTestCase {
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(frames.shown.minY, visibleFrame.minY)
XCTAssertEqual(frames.shown.height, visibleFrame.height)
XCTAssertLessThan(frames.hidden.maxY, visibleFrame.minY)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.width, 320)
XCTAssertLessThanOrEqual(frames.shown.width, visibleFrame.width)
XCTAssertEqual(frames.hidden.minX, screenFrame.maxX + 1)
}
func testPanelFrameIgnoresLegacyPreferredResizableHeight() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 96, width: 1512, height: 861)
let frames = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
preferredHeight: 600
)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.height, 957)
XCTAssertEqual(frames.hidden.minX, visibleFrame.maxX + 1)
}
func testPanelFrameLegacyPreferredHeightDoesNotAffectSideShelf() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let tooTall = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
preferredHeight: 900
)
let tooShort = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
preferredHeight: 120
)
XCTAssertEqual(tooTall.shown.height, 982)
XCTAssertEqual(tooShort.shown.height, 982)
XCTAssertEqual(tooTall.shown.width, 336)
XCTAssertEqual(tooShort.shown.width, 336)
}
func testPanelFrameLegacyPreferredHeightDoesNotOverflowTinyVisibleFrame() {
let screenFrame = CGRect(x: 0, y: 0, width: 640, height: 320)
let visibleFrame = CGRect(x: 0, y: 48, width: 640, height: 220)
let frames = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
preferredHeight: 620
)
XCTAssertEqual(frames.shown.height, 268)
XCTAssertEqual(frames.shown.width, 320)
XCTAssertLessThanOrEqual(frames.shown.height, visibleFrame.maxY - screenFrame.minY)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
}
func testPanelFramePlanningIsDeterministicAcrossRepeatedToggles() {
@@ -68,48 +202,161 @@ final class ClipboardPanelControllerTests: XCTestCase {
let frames = ClipboardPanelController.panelFrames(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(frames.shown, first.shown)
XCTAssertEqual(frames.hidden, first.hidden)
XCTAssertEqual(frames.hidden.maxY, frames.shown.minY - 1)
XCTAssertEqual(frames.hidden.minX, frames.shown.maxX + 1)
}
}
func testPanelAnimationProfileStaysShortForSixtyFpsFeel() {
let profile = ClipboardPanelController.animationProfile
XCTAssertEqual(profile.showDuration, 0.16)
XCTAssertEqual(profile.hideDuration, 0.12)
XCTAssertEqual(profile.reflowDuration, 0.10)
XCTAssertLessThanOrEqual(profile.showDuration * 60, 10)
XCTAssertLessThanOrEqual(profile.hideDuration * 60, 8)
XCTAssertLessThanOrEqual(profile.reflowDuration * 60, 6)
XCTAssertEqual(profile.showDuration, 0.22)
XCTAssertEqual(profile.hideDuration, 0.16)
XCTAssertEqual(profile.reflowDuration, 0.18)
XCTAssertLessThanOrEqual(profile.showDuration * 60, 14)
XCTAssertLessThanOrEqual(profile.hideDuration * 60, 10)
XCTAssertLessThanOrEqual(profile.reflowDuration * 60, 11)
XCTAssertEqual(profile.easing, .easeInEaseOut)
}
func testReflowPlanMovesOpenPanelAboveNewBottomDockVisibleFrame() {
func testPanelCollectionBehaviorStaysLocalToActiveSpaceAndSupportsFullscreen() {
let behavior = ClipboardPanelController.panelCollectionBehavior
XCTAssertTrue(behavior.contains(.moveToActiveSpace))
XCTAssertTrue(behavior.contains(.fullScreenAuxiliary))
XCTAssertTrue(behavior.contains(.transient))
XCTAssertFalse(behavior.contains(.canJoinAllSpaces))
}
func testShowingPanelClearsAndCollapsesStaleSearchState() throws {
let screen = try XCTUnwrap(NSScreen.screens.first)
let (controller, _) = makeController(preferredScreen: screen)
controller.debugSetSearchFieldText("stale query")
drainMainQueue()
XCTAssertEqual(controller.debugSearchFieldText, "stale query")
XCTAssertEqual(controller.debugSearchFieldWidth, 164, accuracy: 0.5)
XCTAssertEqual(controller.debugSearchFieldPlaceholderText, "Search clips")
XCTAssertTrue(controller.debugSearchFieldIsVisible)
XCTAssertFalse(controller.debugSearchIconButtonIsVisible)
controller.show(preferredScreen: screen)
defer { controller.hide(immediate: true) }
drainMainQueue()
XCTAssertEqual(controller.debugSearchFieldText, "")
XCTAssertEqual(controller.debugSearchFieldWidth, 30, accuracy: 1)
XCTAssertEqual(controller.debugSearchFieldPlaceholderText, "")
XCTAssertFalse(controller.debugSearchFieldIsVisible)
XCTAssertTrue(controller.debugSearchIconButtonIsVisible)
XCTAssertFalse(controller.debugIsSearchFieldEditing)
}
func testReflowPlanKeepsOpenPanelOnRightSideWhenBottomDockIsVisible() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 112, width: 1512, height: 845)
let plan = ClipboardPanelController.reflowPlan(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(plan.frame.minX, visibleFrame.minX)
XCTAssertEqual(plan.frame.maxX, visibleFrame.maxX)
XCTAssertEqual(plan.frame.minY, visibleFrame.minY)
XCTAssertEqual(plan.frame.height, 408)
XCTAssertEqual(plan.frame.minY, screenFrame.minY)
XCTAssertEqual(plan.frame.maxY, visibleFrame.maxY)
XCTAssertEqual(plan.frame.width, 336)
XCTAssertEqual(plan.bottomSafeInset, 20)
}
func testReflowPlanTracksSideDockVisibleWidthWithoutBottomInsetInflation() {
func testReflowPlanTouchesScreenBottomWhenBottomDockIsAutoHidden() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 4, width: 1512, height: 953)
let plan = ClipboardPanelController.reflowPlan(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(plan.frame.maxX, visibleFrame.maxX)
XCTAssertEqual(plan.frame.minY, screenFrame.minY)
XCTAssertEqual(plan.frame.maxY, visibleFrame.maxY)
XCTAssertEqual(plan.frame.width, 336)
XCTAssertEqual(plan.bottomSafeInset, 18)
}
func testReflowPlanIgnoresLegacyPreferredResizableHeight() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 96, width: 1512, height: 861)
let plan = ClipboardPanelController.reflowPlan(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
preferredHeight: 600
)
XCTAssertEqual(plan.frame.minY, screenFrame.minY)
XCTAssertEqual(plan.frame.height, 957)
XCTAssertEqual(plan.frame.width, 336)
XCTAssertEqual(plan.bottomSafeInset, 20)
}
func testReflowPlanTracksSideDockVisibleFrameWithoutBottomInsetInflation() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 86, y: 0, width: 1426, height: 957)
let plan = ClipboardPanelController.reflowPlan(forScreenFrame: screenFrame, visibleFrame: visibleFrame)
XCTAssertEqual(plan.frame.minX, visibleFrame.minX)
XCTAssertEqual(plan.frame.maxX, visibleFrame.maxX)
XCTAssertEqual(plan.frame.minY, visibleFrame.minY)
XCTAssertEqual(plan.frame.height, 408)
XCTAssertEqual(plan.frame.minX, visibleFrame.maxX - 336)
XCTAssertEqual(plan.frame.minY, screenFrame.minY)
XCTAssertEqual(plan.frame.height, 957)
XCTAssertEqual(plan.bottomSafeInset, 18)
}
func testPanelFrameUsesConfiguredRightSideShelf() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 96, width: 1512, height: 861)
let frames = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
side: .right
)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.maxX, visibleFrame.maxX)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.hidden.minY, frames.shown.minY)
XCTAssertEqual(frames.hidden.minX, screenFrame.maxX + 1)
}
func testPanelFrameUsesConfiguredLeftSideShelf() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 80, y: 0, width: 1432, height: 957)
let frames = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: visibleFrame,
side: .left
)
XCTAssertEqual(frames.shown.minX, visibleFrame.minX)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, visibleFrame.maxY)
XCTAssertEqual(frames.shown.width, 336)
XCTAssertEqual(frames.hidden.maxX, visibleFrame.minX - 1)
XCTAssertEqual(frames.hidden.minY, frames.shown.minY)
}
func testPanelFrameUsesFullScreenHeightWhenVisibleFrameMatchesScreen() {
let screenFrame = CGRect(x: -1512, y: -120, width: 1512, height: 982)
let frames = ClipboardPanelController.panelFrames(
forScreenFrame: screenFrame,
visibleFrame: screenFrame,
side: .right
)
XCTAssertEqual(frames.shown.minY, screenFrame.minY)
XCTAssertEqual(frames.shown.maxY, screenFrame.maxY)
XCTAssertEqual(frames.shown.maxX, screenFrame.maxX)
XCTAssertEqual(frames.shown.width, 336)
}
func testContentBottomInsetReservesBottomDockSpace() {
let screenFrame = CGRect(x: 0, y: 0, width: 1512, height: 982)
let visibleFrame = CGRect(x: 0, y: 96, width: 1512, height: 861)
@@ -128,6 +375,24 @@ final class ClipboardPanelControllerTests: XCTestCase {
XCTAssertEqual(inset, 18)
}
func testPanelSharingTypeHidesWindowFromScreenCaptureWhenEnabled() {
XCTAssertEqual(ClipboardPanelController.panelSharingType(hideFromScreenCapture: false), .readOnly)
XCTAssertEqual(ClipboardPanelController.panelSharingType(hideFromScreenCapture: true), .none)
}
func testLinkPreviewFrameSitsAboveBottomShelfWhenSpaceAllows() {
let visibleFrame = NSRect(x: 0, y: 0, width: 1512, height: 982)
let shelfFrame = NSRect(x: 0, y: 0, width: 1512, height: 408)
let frame = LinkPreviewWindowController.previewFrame(parentFrame: shelfFrame, visibleFrame: visibleFrame)
XCTAssertGreaterThanOrEqual(frame.minY, shelfFrame.maxY + 14)
XCTAssertLessThanOrEqual(frame.maxY, visibleFrame.maxY - 24)
XCTAssertEqual(frame.width, 1088)
XCTAssertEqual(frame.height, 536)
XCTAssertEqual(frame.midX, shelfFrame.midX, accuracy: 0.5)
}
func testCommandNumberShortcutsMapToQuickPasteSlots() {
XCTAssertEqual(ClipboardPanelController.quickPasteIndex(forKeyCode: 18, modifiers: .command), 0)
XCTAssertEqual(ClipboardPanelController.quickPasteIndex(forKeyCode: 19, modifiers: .command), 1)
@@ -181,20 +446,48 @@ final class ClipboardPanelControllerTests: XCTestCase {
XCTAssertEqual(ClipboardPanelController.navigationShortcutAction(forKeyCode: 121, modifiers: []), .pageNext)
XCTAssertEqual(ClipboardPanelController.navigationShortcutAction(forKeyCode: 116, modifiers: []), .pagePrevious)
XCTAssertEqual(ClipboardPanelController.navigationShortcutAction(forKeyCode: 123, modifiers: []), .previous)
XCTAssertEqual(ClipboardPanelController.navigationShortcutAction(forKeyCode: 126, modifiers: .command), .first)
XCTAssertEqual(ClipboardPanelController.navigationShortcutAction(forKeyCode: 125, modifiers: .command), .last)
}
func testNavigationShortcutsRequireNoModifiers() {
func testNavigationShortcutsRejectUnsupportedModifiers() {
XCTAssertNil(ClipboardPanelController.navigationShortcutAction(forKeyCode: 124, modifiers: .command))
XCTAssertNil(ClipboardPanelController.navigationShortcutAction(forKeyCode: 126, modifiers: []))
XCTAssertNil(ClipboardPanelController.navigationShortcutAction(forKeyCode: 125, modifiers: []))
XCTAssertNil(ClipboardPanelController.navigationShortcutAction(forKeyCode: 121, modifiers: .shift))
XCTAssertNil(ClipboardPanelController.navigationShortcutAction(forKeyCode: 35, modifiers: []))
}
func testSelectionShortcutsMapToRangeAndSelectAllActions() {
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 0, modifiers: .command), .selectAll)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 115, modifiers: .shift), .extendFirst)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 119, modifiers: .shift), .extendLast)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 124, modifiers: .shift), .extendNext)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 121, modifiers: .shift), .extendPageNext)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 116, modifiers: .shift), .extendPagePrevious)
XCTAssertEqual(ClipboardPanelController.selectionShortcutAction(forKeyCode: 123, modifiers: .shift), .extendPrevious)
}
func testSelectionShortcutsRequireExactModifierSets() {
XCTAssertNil(ClipboardPanelController.selectionShortcutAction(forKeyCode: 0, modifiers: []))
XCTAssertNil(ClipboardPanelController.selectionShortcutAction(forKeyCode: 0, modifiers: [.command, .shift]))
XCTAssertNil(ClipboardPanelController.selectionShortcutAction(forKeyCode: 124, modifiers: []))
XCTAssertNil(ClipboardPanelController.selectionShortcutAction(forKeyCode: 124, modifiers: [.command, .shift]))
}
func testCommandActionShortcutsMapToSelectedClipActions() {
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 8, modifiers: .command), .copy)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 14, modifiers: .command), .edit)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 3, modifiers: .command), .focusSearch)
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 45, modifiers: .command))
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 5, modifiers: .command), .showInClipboard)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 16, modifiers: .command), .preview)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 31, modifiers: .command), .open)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 15, modifiers: .command), .reveal)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 15, modifiers: .command), .rename)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 17, modifiers: .command), .toggleCapturePause)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 6, modifiers: .command), .undoDelete)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 123, modifiers: .command), .previousCollection)
XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 124, modifiers: .command), .nextCollection)
}
func testCommandActionShortcutsRequireCommandOnlySoSearchTypingIsUntouched() {
@@ -203,17 +496,68 @@ final class ClipboardPanelControllerTests: XCTestCase {
XCTAssertNil(ClipboardPanelController.commandShortcutAction(forKeyCode: 9, modifiers: .command))
}
func testModifiedShortcutsMapToPlainTextActions() {
func testModifiedShortcutsMapToPanelActions() {
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 36, modifiers: .shift), .pastePlainText)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 1, modifiers: [.command, .shift]), .toggleStack)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .toggleStackCapture)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 45, modifiers: [.command, .shift]), .newCollection)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 36, modifiers: [.command, .shift]), .pasteStackNext)
}
func testModifiedShortcutsRequireCommandShiftOnly() {
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 36, modifiers: []))
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: .shift))
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: .command))
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .option, .shift]))
XCTAssertNil(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 31, modifiers: [.command, .shift]))
}
private func makeController(preferredScreen: NSScreen) -> (ClipboardPanelController, ClipboardStore) {
let settings = makeSettings()
let encryptionService = ClipboardEncryptionService(keyProvider: { nil })
let cacheService = ClipboardCacheService(
baseURL: makeTempDirectory(),
encryptionService: encryptionService
)
let store = ClipboardStore(
settings: settings,
cacheService: cacheService,
baseURL: makeTempDirectory(),
encryptionService: encryptionService
)
let controller = ClipboardPanelController(
store: store,
settings: settings,
cacheService: cacheService,
preferredScreen: { preferredScreen }
)
return (controller, store)
}
private func makeSettings() -> SettingsModel {
let suite = "com.clipbored.controllertest.\(UUID().uuidString)"
defaultsSuites.append(suite)
let defaults = UserDefaults(suiteName: suite)!
let settings = SettingsModel(defaults: defaults)
settings.maxHistoryItems = 10
settings.historyRetention = .forever
settings.pruneDuplicates = false
return settings
}
private func makeTempDirectory() -> URL {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("clipbored-controllertest")
.appendingPathComponent(UUID().uuidString)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
tempURLs.append(directory)
return directory
}
private func drainMainQueue() {
for _ in 0..<24 {
RunLoop.main.run(until: Date().addingTimeInterval(0.01))
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -237,10 +237,12 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertEqual(store.items.first?.id, itemID)
XCTAssertEqual(store.items.first?.payload, "legacy payload")
XCTAssertEqual(store.items.first?.useCount, 3)
XCTAssertEqual(store.items.first?.sourceDeviceName, ClipboardItem.localDeviceName)
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertEqual(restored.items.first?.payload, "legacy payload")
XCTAssertEqual(restored.items.first?.sourceDeviceName, ClipboardItem.localDeviceName)
}
func testPinnedItemsSurviveNormalHistoryPrune() {
@@ -266,6 +268,114 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertTrue(restored.items.contains(where: { $0.payload == "pinned-old" && $0.isPinned }))
}
func testCollectionItemsSurviveNormalHistoryPrune() {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
let start = Date()
var saved = makeItem("collection-old", displayText: "Saved", created: start.addingTimeInterval(-500))
saved.collectionName = "Client Work"
store.upsert(saved)
for index in 0..<60 {
store.upsert(makeItem("new-\(index)", displayText: "New \(index)", created: start.addingTimeInterval(Double(index))))
}
store.flushPersistenceForTesting()
XCTAssertTrue(store.items.contains(where: { $0.payload == "collection-old" && $0.collectionName == "Client Work" }))
XCTAssertEqual(store.items.filter { !$0.isPinned && $0.collectionName == nil }.count, 50)
XCTAssertEqual(store.items.count, 51)
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertTrue(restored.items.contains(where: { $0.payload == "collection-old" && $0.collectionName == "Client Work" }))
}
func testRemovingCollectionMakesOverflowItemEligibleForHistoryPrune() {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
let start = Date()
var saved = makeItem("collection-old", displayText: "Saved", created: start.addingTimeInterval(-500))
saved.collectionName = "Client Work"
store.upsert(saved)
for index in 0..<60 {
store.upsert(makeItem("new-\(index)", displayText: "New \(index)", created: start.addingTimeInterval(Double(index))))
}
let savedID = try! XCTUnwrap(store.items.first(where: { $0.payload == "collection-old" })?.id)
store.setCollection(savedID, name: nil)
store.flushPersistenceForTesting()
XCTAssertFalse(store.items.contains(where: { $0.payload == "collection-old" }))
XCTAssertEqual(store.items.count, 50)
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertFalse(restored.items.contains(where: { $0.payload == "collection-old" }))
}
func testHistoryRetentionPrunesExpiredUnpinnedItems() {
let settings = makeSettings(maxHistory: 50)
settings.historyRetention = .oneDay
let store = makeStore(settings: settings)
let now = Date()
store.upsert(makeItem("old", displayText: "Old", created: now.addingTimeInterval(-2 * 24 * 60 * 60)))
store.upsert(makeItem("recent", displayText: "Recent", created: now.addingTimeInterval(-60)))
store.flushPersistenceForTesting()
XCTAssertEqual(store.items.map(\.payload), ["recent"])
XCTAssertFalse(store.items.contains(where: { $0.payload == "old" }))
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertEqual(restored.items.map(\.payload), ["recent"])
}
func testHistoryRetentionKeepsPinnedAndCollectionItems() {
let settings = makeSettings(maxHistory: 50)
settings.historyRetention = .oneDay
let store = makeStore(settings: settings)
let oldDate = Date().addingTimeInterval(-2 * 24 * 60 * 60)
var pinned = makeItem("pinned-old", displayText: "Pinned", created: oldDate)
pinned.isPinned = true
var saved = makeItem("collection-old", displayText: "Saved", created: oldDate)
saved.collectionName = "Client Work"
store.upsert(makeItem("plain-old", displayText: "Plain", created: oldDate))
store.upsert(pinned)
store.upsert(saved)
store.flushPersistenceForTesting()
XCTAssertFalse(store.items.contains(where: { $0.payload == "plain-old" }))
XCTAssertTrue(store.items.contains(where: { $0.payload == "pinned-old" && $0.isPinned }))
XCTAssertTrue(store.items.contains(where: { $0.payload == "collection-old" && $0.collectionName == "Client Work" }))
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertFalse(restored.items.contains(where: { $0.payload == "plain-old" }))
XCTAssertTrue(restored.items.contains(where: { $0.payload == "pinned-old" && $0.isPinned }))
XCTAssertTrue(restored.items.contains(where: { $0.payload == "collection-old" && $0.collectionName == "Client Work" }))
}
func testDuplicateCopyRefreshesRetentionAge() {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
let oldDate = Date().addingTimeInterval(-2 * 24 * 60 * 60)
store.upsert(makeItem("same", displayText: "Original", created: oldDate))
let originalCreatedAt = try! XCTUnwrap(store.items.first?.createdAt)
store.upsert(makeItem("same", displayText: "Copied again", created: oldDate))
XCTAssertGreaterThan(try! XCTUnwrap(store.items.first?.createdAt), originalCreatedAt)
settings.historyRetention = .oneDay
store.normalizeHistoryLength()
XCTAssertEqual(store.items.map(\.payload), ["same"])
XCTAssertEqual(store.items.first?.displayText, "Copied again")
}
func testStorageFilesUsePrivatePermissions() throws {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
@@ -360,7 +470,8 @@ final class ClipboardStoreTests: XCTestCase {
isPinned: false,
sourceAppBundleId: "com.example.secret.\(UUID().uuidString)",
ocrText: "OCR secret \(UUID().uuidString)",
collectionName: "Collection secret \(UUID().uuidString)"
collectionName: "Collection secret \(UUID().uuidString)",
sourceDeviceName: "Device secret \(UUID().uuidString)"
)
store.upsert(item)
@@ -375,6 +486,7 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertFalse(rawDatabaseText.contains(item.sourceAppBundleId!))
XCTAssertFalse(rawDatabaseText.contains(item.ocrText!))
XCTAssertFalse(rawDatabaseText.contains(item.collectionName!))
XCTAssertFalse(rawDatabaseText.contains(item.sourceDeviceName!))
let restored = makeStore(settings: settings, encryptionService: encryptionService)
restored.flushPersistenceForTesting()
@@ -386,6 +498,7 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertEqual(restored.items.first?.sourceAppBundleId, item.sourceAppBundleId)
XCTAssertEqual(restored.items.first?.ocrText, item.ocrText)
XCTAssertEqual(restored.items.first?.collectionName, item.collectionName)
XCTAssertEqual(restored.items.first?.sourceDeviceName, item.sourceDeviceName)
}
func testPlaintextDatabaseMigratesToEncryptedFieldsOnLoad() throws {
@@ -406,7 +519,8 @@ final class ClipboardStoreTests: XCTestCase {
isPinned: false,
sourceAppBundleId: "com.example.legacy.\(UUID().uuidString)",
ocrText: "Legacy OCR \(UUID().uuidString)",
collectionName: "Legacy collection \(UUID().uuidString)"
collectionName: "Legacy collection \(UUID().uuidString)",
sourceDeviceName: "Legacy device \(UUID().uuidString)"
)
plaintextStore.upsert(item)
@@ -424,6 +538,7 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertEqual(restored.items.first?.sourceAppBundleId, item.sourceAppBundleId)
XCTAssertEqual(restored.items.first?.ocrText, item.ocrText)
XCTAssertEqual(restored.items.first?.collectionName, item.collectionName)
XCTAssertEqual(restored.items.first?.sourceDeviceName, item.sourceDeviceName)
let migratedDatabaseText = try databaseText()
XCTAssertTrue(migratedDatabaseText.contains(ClipboardEncryptionService.marker))
@@ -434,6 +549,7 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertFalse(migratedDatabaseText.contains(item.sourceAppBundleId!))
XCTAssertFalse(migratedDatabaseText.contains(item.ocrText!))
XCTAssertFalse(migratedDatabaseText.contains(item.collectionName!))
XCTAssertFalse(migratedDatabaseText.contains(item.sourceDeviceName!))
}
func testDuplicatePDFReplacementRemovesOldAttachment() throws {
@@ -454,6 +570,47 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertTrue(FileManager.default.fileExists(atPath: newPath))
}
func testRemoveCanDeferManagedCachePurgeForUndoRestore() throws {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
let cachedImage = try XCTUnwrap(cacheService.cacheImage(makeImage(color: .systemBlue), id: UUID()))
let item = makeImageItem(
fullPath: cachedImage.full,
thumbPath: cachedImage.thumb,
hash: "undo-image",
ocrText: "undo searchable text",
created: Date(timeIntervalSince1970: 10)
)
store.upsert(item)
store.flushPersistenceForTesting()
let removal = try XCTUnwrap(store.remove(item.id, purgeManagedCache: false))
store.flushPersistenceForTesting()
cacheService.flushForTesting()
XCTAssertTrue(store.items.isEmpty)
XCTAssertTrue(FileManager.default.fileExists(atPath: cachedImage.full))
XCTAssertTrue(FileManager.default.fileExists(atPath: cachedImage.thumb))
store.restore([removal])
store.flushPersistenceForTesting()
XCTAssertEqual(store.items.map(\.id), [item.id])
XCTAssertTrue(FileManager.default.fileExists(atPath: cachedImage.full))
XCTAssertTrue(FileManager.default.fileExists(atPath: cachedImage.thumb))
let restored = makeStore(settings: settings)
restored.flushPersistenceForTesting()
XCTAssertEqual(restored.items.map(\.id), [item.id])
_ = store.remove(item.id)
store.flushPersistenceForTesting()
cacheService.flushForTesting()
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImage.full))
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImage.thumb))
}
func testDuplicateReplacementClearsStaleImageSearchMetadata() throws {
let settings = makeSettings(maxHistory: 50)
settings.keepFirstImage = false
@@ -501,9 +658,204 @@ final class ClipboardStoreTests: XCTestCase {
XCTAssertFalse(FileManager.default.fileExists(atPath: staleImage.thumb))
}
func testArchiveExportImportMovesHistoryAndManagedAttachmentsToNewStorage() throws {
let settings = makeSettings(maxHistory: 50)
let store = makeStore(settings: settings)
let cachedImage = try XCTUnwrap(cacheService.cacheImage(makeImage(color: .systemBlue), id: UUID()))
let sourceFullImageData = try XCTUnwrap(cacheService.data(for: cachedImage.full))
let sourceThumbData = try XCTUnwrap(cacheService.data(for: cachedImage.thumb))
let sourcePDFData = Data("%PDF archive payload".utf8)
let sourcePDFPath = try XCTUnwrap(cacheService.cachePDF(sourcePDFData, id: UUID()))
let created = Date(timeIntervalSince1970: 300)
var note = makeItem("archive note", displayText: "Archive Note", created: created)
note.collectionName = "Read Later"
note.customTitle = "Migration Note"
note.sourceDeviceName = "Studio Mac"
var image = makeImageItem(
fullPath: cachedImage.full,
thumbPath: cachedImage.thumb,
hash: "archive-image",
ocrText: "blue archive image",
created: created.addingTimeInterval(1)
)
image.isPinned = true
image.collectionName = "Client Work"
image.customTitle = "Launch Screenshot"
image.sourceDeviceName = "MacBook Pro"
var pdf = makePDFItem(
path: sourcePDFPath,
hash: "archive-pdf",
created: created.addingTimeInterval(2)
)
pdf.collectionName = "Research"
pdf.sourceDeviceName = "Design Mac"
store.upsert(note)
store.upsert(image)
store.upsert(pdf)
store.flushPersistenceForTesting()
let archiveURL = baseURL.appendingPathComponent("backup.clipboredarchive")
let exportSummary = try store.exportArchive(to: archiveURL)
XCTAssertEqual(exportSummary.itemCount, 3)
XCTAssertEqual(exportSummary.sidecarCount, 3)
XCTAssertEqual(try posixPermissions(archiveURL), 0o600)
let destinationBaseURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clipboredtests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: destinationBaseURL, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: destinationBaseURL) }
let destinationDefaultsSuite = "com.clipbored.archiveimport.\(UUID().uuidString)"
let destinationDefaults = try XCTUnwrap(UserDefaults(suiteName: destinationDefaultsSuite))
destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite)
defer { destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite) }
let destinationSettings = SettingsModel(defaults: destinationDefaults)
destinationSettings.maxHistoryItems = 50
destinationSettings.historyRetention = .forever
let destinationCache = ClipboardCacheService(
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
let destinationStore = ClipboardStore(
settings: destinationSettings,
cacheService: destinationCache,
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
destinationStore.upsert(makeItem("destination only", displayText: "Destination", created: created.addingTimeInterval(3)))
destinationStore.flushPersistenceForTesting()
let importSummary = try destinationStore.importArchive(from: archiveURL)
destinationStore.flushPersistenceForTesting()
XCTAssertEqual(importSummary.itemCount, 3)
XCTAssertEqual(importSummary.sidecarCount, 3)
XCTAssertEqual(destinationStore.items.count, 4)
XCTAssertTrue(destinationStore.items.contains { $0.payload == "destination only" })
let importedNote = try XCTUnwrap(destinationStore.items.first { $0.id == note.id })
XCTAssertEqual(importedNote.collectionName, "Read Later")
XCTAssertEqual(importedNote.customTitle, "Migration Note")
XCTAssertEqual(importedNote.sourceDeviceName, "Studio Mac")
let importedImage = try XCTUnwrap(destinationStore.items.first { $0.id == image.id })
XCTAssertTrue(importedImage.isPinned)
XCTAssertEqual(importedImage.collectionName, "Client Work")
XCTAssertEqual(importedImage.customTitle, "Launch Screenshot")
XCTAssertEqual(importedImage.sourceDeviceName, "MacBook Pro")
let importedImagePath = try XCTUnwrap(importedImage.imagePath)
let importedThumbPath = try XCTUnwrap(importedImage.thumbnailPath)
XCTAssertTrue(importedImagePath.hasPrefix(destinationBaseURL.path))
XCTAssertTrue(importedThumbPath.hasPrefix(destinationBaseURL.path))
XCTAssertNotEqual(importedImagePath, cachedImage.full)
XCTAssertEqual(destinationCache.data(for: importedImagePath), sourceFullImageData)
XCTAssertEqual(destinationCache.data(for: importedThumbPath), sourceThumbData)
let importedPDF = try XCTUnwrap(destinationStore.items.first { $0.id == pdf.id })
XCTAssertEqual(importedPDF.collectionName, "Research")
XCTAssertTrue(importedPDF.payload.hasPrefix(destinationBaseURL.path))
XCTAssertNotEqual(importedPDF.payload, sourcePDFPath)
XCTAssertEqual(destinationCache.data(for: importedPDF.payload), sourcePDFData)
let reloaded = ClipboardStore(
settings: destinationSettings,
cacheService: destinationCache,
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
reloaded.flushPersistenceForTesting()
XCTAssertEqual(reloaded.items.count, 4)
XCTAssertTrue(reloaded.items.contains { $0.id == image.id && $0.collectionName == "Client Work" })
XCTAssertTrue(reloaded.items.contains { $0.id == pdf.id && $0.collectionName == "Research" })
}
func testPinboardExportImportsOnlyThatCollectionAndPreservesColor() throws {
let settings = makeSettings(maxHistory: 50)
settings.ensureCollection(named: "Client Work", colorHex: "#3366FF")
let store = makeStore(settings: settings)
let created = Date(timeIntervalSince1970: 500)
var client = makeItem("client note", displayText: "Client Note", created: created)
client.collectionName = "Client Work"
let unrelated = makeItem("other note", displayText: "Other Note", created: created.addingTimeInterval(1))
store.upsert(client)
store.upsert(unrelated)
store.flushPersistenceForTesting()
let archiveURL = baseURL.appendingPathComponent("client-work.clipboredarchive")
let exportSummary = try store.exportCollection(named: "Client Work", to: archiveURL)
XCTAssertEqual(exportSummary.itemCount, 1)
XCTAssertEqual(exportSummary.sidecarCount, 0)
let destinationDefaultsSuite = "com.clipbored.pinboardimport.\(UUID().uuidString)"
let destinationDefaults = try XCTUnwrap(UserDefaults(suiteName: destinationDefaultsSuite))
destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite)
defer { destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite) }
let destinationSettings = SettingsModel(defaults: destinationDefaults)
destinationSettings.maxHistoryItems = 50
destinationSettings.historyRetention = .forever
let destinationBaseURL = baseURL.appendingPathComponent("pinboard-destination", isDirectory: true)
let destinationCache = ClipboardCacheService(
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
let destinationStore = ClipboardStore(
settings: destinationSettings,
cacheService: destinationCache,
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
let importSummary = try destinationStore.importArchive(from: archiveURL)
XCTAssertEqual(importSummary.itemCount, 1)
XCTAssertEqual(destinationStore.items.map(\.payload), ["client note"])
XCTAssertEqual(destinationStore.items.first?.collectionName, "Client Work")
XCTAssertEqual(destinationSettings.customCollectionNames, ["Client Work"])
XCTAssertEqual(destinationSettings.collectionColorHex(forCollectionNamed: "Client Work"), "#3366FF")
}
func testEmptyPinboardExportImportsCollectionMetadata() throws {
let settings = makeSettings(maxHistory: 50)
settings.ensureCollection(named: "Read Later", colorHex: "#0A9EB8")
let store = makeStore(settings: settings)
let archiveURL = baseURL.appendingPathComponent("read-later-empty.clipboredarchive")
let exportSummary = try store.exportCollection(named: "Read Later", to: archiveURL)
XCTAssertEqual(exportSummary.itemCount, 0)
let destinationDefaultsSuite = "com.clipbored.emptypinboard.\(UUID().uuidString)"
let destinationDefaults = try XCTUnwrap(UserDefaults(suiteName: destinationDefaultsSuite))
destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite)
defer { destinationDefaults.removePersistentDomain(forName: destinationDefaultsSuite) }
let destinationSettings = SettingsModel(defaults: destinationDefaults)
let destinationBaseURL = baseURL.appendingPathComponent("empty-pinboard-destination", isDirectory: true)
let destinationStore = ClipboardStore(
settings: destinationSettings,
cacheService: ClipboardCacheService(baseURL: destinationBaseURL, encryptionService: noOpEncryptionService()),
baseURL: destinationBaseURL,
encryptionService: noOpEncryptionService()
)
let importSummary = try destinationStore.importArchive(from: archiveURL)
XCTAssertEqual(importSummary.itemCount, 0)
XCTAssertTrue(destinationStore.items.isEmpty)
XCTAssertEqual(destinationSettings.customCollectionNames, ["Read Later"])
XCTAssertEqual(destinationSettings.collectionColorHex(forCollectionNamed: "Read Later"), "#0A9EB8")
}
private func makeSettings(maxHistory: Int) -> SettingsModel {
let settings = SettingsModel(defaults: defaults)
settings.maxHistoryItems = maxHistory
settings.historyRetention = .forever
settings.pruneDuplicates = true
settings.keepFirstImage = true
return settings

View File

@@ -12,8 +12,8 @@ final class DiagnosticsServiceTests: XCTestCase {
diagnostics.incrementDatabaseMutation()
diagnostics.incrementCachePurge()
// The counters use a serial async queue for low overhead; sync through reset's queue by reading a snapshot.
let snapshot = waitForSnapshot { diagnostics.currentSnapshot() }
// The snapshot read synchronizes with the serial diagnostics queue.
let snapshot = diagnostics.currentSnapshot()
XCTAssertEqual(snapshot.monitorTicks, 1)
XCTAssertEqual(snapshot.pasteboardChanges, 1)
XCTAssertEqual(snapshot.extractionAttempts, 1)
@@ -23,13 +23,4 @@ final class DiagnosticsServiceTests: XCTestCase {
diagnostics.reset()
XCTAssertEqual(diagnostics.currentSnapshot(), .init(monitorTicks: 0, pasteboardChanges: 0, extractionAttempts: 0, databaseMutations: 0, cachePurges: 0))
}
private func waitForSnapshot(_ snapshot: @escaping () -> DiagnosticsService.Snapshot) -> DiagnosticsService.Snapshot {
let expectation = expectation(description: "diagnostics queue")
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
return snapshot()
}
}

View File

@@ -0,0 +1,81 @@
import XCTest
@testable import ClipBored
final class LinkPreviewWindowControllerTests: XCTestCase {
func testReusedPreviewIgnoresStaleObservedTitleUntilNewNavigationStarts() throws {
let controller = LinkPreviewWindowController()
let firstURL = try XCTUnwrap(URL(string: "https://example.com/old"))
let secondURL = try XCTUnwrap(URL(string: "https://example.com/new"))
controller.debugPrepareForPreview(LinkPreviewRequest(url: firstURL, title: "Old request title"))
controller.debugAllowObservedPageTitles()
controller.debugApplyObservedPageTitle("Old loaded title")
XCTAssertEqual(controller.debugTitleText, "Old loaded title")
controller.debugPrepareForPreview(LinkPreviewRequest(url: secondURL, title: "New request title"))
controller.debugApplyObservedPageTitle("Old loaded title")
XCTAssertEqual(controller.debugTitleText, "New request title")
XCTAssertEqual(controller.debugAddressText, "https://example.com/new")
XCTAssertEqual(controller.debugStatusText, "Loading")
controller.debugAllowObservedPageTitles()
controller.debugApplyObservedPageTitle("New loaded title")
XCTAssertEqual(controller.debugTitleText, "New loaded title")
}
func testCancelledNavigationDoesNotShowFalseLoadFailure() throws {
let controller = LinkPreviewWindowController()
let url = try XCTUnwrap(URL(string: "https://example.com/new"))
controller.debugPrepareForPreview(LinkPreviewRequest(url: url, title: "New request title"))
controller.debugApplyNavigationFailure(URLError(.cancelled))
XCTAssertEqual(controller.debugStatusText, "Loading")
controller.debugApplyNavigationFailure(URLError(.timedOut))
XCTAssertEqual(controller.debugStatusText, "Could not load")
}
func testToolbarTooltipsTrackFullVisibleText() throws {
let controller = LinkPreviewWindowController()
let url = try XCTUnwrap(URL(string: "https://example.com/articles/a-very-long-release-note-title?ref=clipbored"))
controller.debugPrepareForPreview(LinkPreviewRequest(url: url, title: "Release note with a long title"))
XCTAssertEqual(controller.debugTitleTooltip, "Release note with a long title")
XCTAssertEqual(controller.debugAddressTooltip, url.absoluteString)
XCTAssertEqual(controller.debugStatusTooltip, "Loading")
controller.debugAllowObservedPageTitles()
controller.debugApplyObservedPageTitle("Loaded page title that may truncate in the toolbar")
XCTAssertEqual(controller.debugTitleTooltip, "Loaded page title that may truncate in the toolbar")
controller.debugApplyNavigationFailure(URLError(.timedOut))
XCTAssertEqual(controller.debugStatusTooltip, "Could not load")
}
func testOpenInBrowserUsesDisplayedPageURLAndResetsForReusedPreview() throws {
var openedURLs: [URL] = []
let controller = LinkPreviewWindowController { openedURLs.append($0) }
let firstURL = try XCTUnwrap(URL(string: "https://example.com/old"))
let navigatedURL = try XCTUnwrap(URL(string: "https://example.com/old/details"))
let secondURL = try XCTUnwrap(URL(string: "https://example.com/new"))
controller.debugPrepareForPreview(LinkPreviewRequest(url: firstURL, title: "Old request title"))
controller.debugSetDisplayedPageURL(navigatedURL)
controller.debugOpenInBrowser()
XCTAssertEqual(openedURLs, [navigatedURL])
controller.debugPrepareForPreview(LinkPreviewRequest(url: secondURL, title: "New request title"))
controller.debugOpenInBrowser()
XCTAssertEqual(openedURLs, [navigatedURL, secondURL])
}
}

View File

@@ -0,0 +1,75 @@
import AppKit
import XCTest
@testable import ClipBored
final class OnboardingWindowControllerTests: XCTestCase {
func testPasteStyleShortcutUsesShiftCommandV() {
let shortcut = OnboardingWindowController.pasteStyleOpenShortcut
XCTAssertEqual(shortcut.key, "v")
XCTAssertTrue(shortcut.has(.command))
XCTAssertTrue(shortcut.has(.shift))
XCTAssertFalse(shortcut.has(.option))
XCTAssertFalse(shortcut.has(.control))
}
func testFreshDefaultShortcutStartsWithPasteStyleChoice() {
let choice = OnboardingWindowController.initialShortcutChoice(
for: AppConfiguration.defaultOpenShortcut,
onboardingCompleted: false
)
XCTAssertEqual(choice, .pasteStyle)
}
func testCompletedDefaultShortcutKeepsClipBoredDefaultChoice() {
let choice = OnboardingWindowController.initialShortcutChoice(
for: AppConfiguration.defaultOpenShortcut,
onboardingCompleted: true
)
XCTAssertEqual(choice, .clipBoredDefault)
}
func testPresentationChoiceKeepsAVisibleEntryPoint() {
let presentation = OnboardingWindowController.normalizedPresentation(
showMenuBarIcon: false,
showDockIcon: false
)
XCTAssertTrue(presentation.showMenuBarIcon)
XCTAssertFalse(presentation.showDockIcon)
}
func testPresentationChoicePreservesExplicitDockOnlySetup() {
let presentation = OnboardingWindowController.normalizedPresentation(
showMenuBarIcon: false,
showDockIcon: true
)
XCTAssertFalse(presentation.showMenuBarIcon)
XCTAssertTrue(presentation.showDockIcon)
}
func testDisplayedEntryPointSelectionIsNormalizedOnRefresh() {
let settings = makeSettings()
settings.showMenuBarIcon = false
settings.showDockIcon = false
let controller = OnboardingWindowController(
settings: settings,
onOpenAccessibility: {},
onFinish: {}
)
XCTAssertTrue(controller.debugShowMenuBarIconIsEnabled)
XCTAssertFalse(controller.debugShowDockIconIsEnabled)
}
private func makeSettings() -> SettingsModel {
let suiteName = "com.clipbored.onboarding.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return SettingsModel(defaults: defaults)
}
}

View File

@@ -13,7 +13,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyWritesTextToPasteboard() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .text,
@@ -33,7 +33,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyWritesCodeSnippetAsPlainString() {
let service = PasteActionService()
let service = makeService()
let snippet = "func greet(name: String) -> String {\n return \"Hi \\(name)\"\n}"
let item = ClipboardItem(
id: UUID(),
@@ -54,7 +54,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testPasteboardWritersExposeTextForDragOut() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .text,
@@ -77,7 +77,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testPasteboardWritersExposeURLAndTitleForDragOut() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .url,
@@ -104,8 +104,64 @@ final class PasteActionServiceTests: XCTestCase {
)
}
func testCopyMultipleItemsPreservesOriginalPasteboardRepresentations() throws {
let service = makeService()
let link = ClipboardItem(
id: UUID(),
kind: .url,
displayText: "Apple",
payload: "https://apple.com",
payloadHash: "hash",
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: nil,
imagePath: nil,
thumbnailPath: nil
)
let text = makeTextItem("Batch note")
XCTAssertEqual(service.copy([link, text]), .copied)
let pasteboardItems = try XCTUnwrap(NSPasteboard.general.pasteboardItems)
XCTAssertEqual(pasteboardItems.count, 2)
XCTAssertEqual(pasteboardItems[0].string(forType: .URL), "https://apple.com")
XCTAssertEqual(
pasteboardItems[0].string(forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")),
"Apple"
)
XCTAssertEqual(pasteboardItems[1].string(forType: .string), "Batch note")
}
func testCopyMultipleItemsDoesNotClearPasteboardWhenAnyItemIsUnwritable() {
let service = makeService()
let missingPath = FileManager.default.temporaryDirectory
.appendingPathComponent("clipbored-missing-\(UUID().uuidString)")
.path
let missingFile = ClipboardItem(
id: UUID(),
kind: .file,
displayText: "Missing file",
payload: missingPath,
payloadHash: "hash",
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: nil,
imagePath: nil,
thumbnailPath: nil
)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString("keep me", forType: .string)
XCTAssertEqual(
service.copy([makeTextItem("Writable"), missingFile]),
.failed("Could not write items to clipboard.")
)
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "keep me")
}
func testCopyWritesColorToPasteboardWithHexFallback() throws {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .color,
@@ -129,7 +185,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testPasteWithoutTargetCopiesWithoutRequestingAutomaticPaste() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .text,
@@ -152,7 +208,7 @@ final class PasteActionServiceTests: XCTestCase {
var activatedProcessID: pid_t?
let targetApp = try makeRunningTargetApp()
var didScheduleKeyboardPaste = false
let service = PasteActionService(
let service = makeService(
accessibilityPermissionProvider: { true },
targetActivator: { app in
activatedProcessID = app.processIdentifier
@@ -173,7 +229,7 @@ final class PasteActionServiceTests: XCTestCase {
var activatedProcessID: pid_t?
let targetApp = try makeRunningTargetApp()
var didScheduleKeyboardPaste = false
let service = PasteActionService(
let service = makeService(
accessibilityPermissionProvider: { true },
targetActivator: { app in
activatedProcessID = app.processIdentifier
@@ -207,7 +263,7 @@ final class PasteActionServiceTests: XCTestCase {
func testAutomaticPasteDoesNotPostShortcutWhenTargetActivationFails() throws {
var didAttemptActivation = false
let targetApp = try makeRunningTargetApp()
let service = PasteActionService(
let service = makeService(
accessibilityPermissionProvider: { true },
targetActivator: { _ in
didAttemptActivation = true
@@ -225,7 +281,7 @@ final class PasteActionServiceTests: XCTestCase {
func testAutomaticPasteWithoutPermissionDoesNotActivateTarget() throws {
let targetApp = try makeRunningTargetApp()
let service = PasteActionService(
let service = makeService(
accessibilityPermissionProvider: { false },
targetActivator: { _ in
XCTFail("Target should not be activated without Accessibility permission")
@@ -241,7 +297,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyMissingFileDoesNotClearExistingPasteboard() {
let service = PasteActionService()
let service = makeService()
let board = NSPasteboard.general
board.clearContents()
XCTAssertTrue(board.setString("keep me", forType: .string))
@@ -265,7 +321,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyEmptyTextDoesNotClearExistingPasteboard() {
let service = PasteActionService()
let service = makeService()
let board = NSPasteboard.general
board.clearContents()
XCTAssertTrue(board.setString("keep me", forType: .string))
@@ -289,7 +345,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyWritesURLType() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .url,
@@ -376,7 +432,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyLegacyRichTextWritesPlainPayloadWhenRTFCacheIsUnavailable() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .richText,
@@ -398,7 +454,7 @@ final class PasteActionServiceTests: XCTestCase {
func testCopyRichTextWithMissingCacheWritesDisplayTextInsteadOfPath() throws {
let missingPath = try makeTempDirectory().appendingPathComponent("missing-rich-text.rtf").path
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .richText,
@@ -419,7 +475,7 @@ final class PasteActionServiceTests: XCTestCase {
}
func testCopyPlainTextForURLOmitsURLPasteboardTypes() {
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .url,
@@ -442,7 +498,7 @@ final class PasteActionServiceTests: XCTestCase {
func testCopyWritesFileReferenceType() throws {
let fileURL = try makeTempFile(contents: "file contents")
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .file,
@@ -467,7 +523,7 @@ final class PasteActionServiceTests: XCTestCase {
let firstURL = try makeTempFile(contents: "first file")
let secondURL = try makeTempFile(contents: "second file")
let payload = FilePayload.payload(from: [firstURL, secondURL])
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .file,
@@ -491,7 +547,7 @@ final class PasteActionServiceTests: XCTestCase {
func testCopyWritesPDFData() throws {
let pdfData = Data("%PDF-1.4\nclipbored\n%%EOF".utf8)
let fileURL = try makeTempFile(contents: pdfData)
let service = PasteActionService()
let service = makeService()
let item = ClipboardItem(
id: UUID(),
kind: .pdf,
@@ -585,6 +641,36 @@ final class PasteActionServiceTests: XCTestCase {
XCTAssertEqual(NSPasteboard.general.data(forType: .pdf), pdfData)
}
private func makeService(
accessibilityPermissionProvider: @escaping () -> Bool = { false },
targetActivator: @escaping (NSRunningApplication) -> Bool = { _ in false },
keyboardPasteScheduler: @escaping (@escaping () -> Void) -> Void = { _ in }
) -> PasteActionService {
PasteActionService(
cacheService: makeNoOpCacheService(),
accessibilityPermissionProvider: accessibilityPermissionProvider,
targetActivator: targetActivator,
keyboardPasteScheduler: keyboardPasteScheduler
)
}
private func makeNoOpCacheService() -> ClipboardCacheService {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("clipboredtests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
tempURLs.append(directory)
return ClipboardCacheService(baseURL: directory, encryptionService: noOpEncryptionService())
} catch {
XCTFail("Could not create no-op cache directory: \(error)")
return ClipboardCacheService(
baseURL: FileManager.default.temporaryDirectory,
encryptionService: noOpEncryptionService()
)
}
}
private func makeTempFile(contents: String) throws -> URL {
try makeTempFile(contents: Data(contents.utf8))
}
@@ -633,4 +719,8 @@ final class PasteActionServiceTests: XCTestCase {
let keyData = Data(repeating: byte, count: 32)
return ClipboardEncryptionService(keyProvider: { SymmetricKey(data: keyData) })
}
private func noOpEncryptionService() -> ClipboardEncryptionService {
ClipboardEncryptionService(keyProvider: { nil })
}
}

View File

@@ -2,6 +2,43 @@ import XCTest
@testable import ClipBored
final class SettingsModelTests: XCTestCase {
func testFreshProfileRequiresOnboardingUntilCompleted() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertFalse(settings.onboardingCompleted)
XCTAssertFalse(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
settings.markOnboardingCompleted()
XCTAssertTrue(settings.onboardingCompleted)
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
XCTAssertEqual(changes, [.other])
let restored = SettingsModel(defaults: defaults)
XCTAssertTrue(restored.onboardingCompleted)
}
func testLegacyProfileDoesNotShowOnboardingAfterUpgrade() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
defaults.set(AppConfiguration.defaultHistoryLength, forKey: SettingsModel.Keys.maxHistoryItems)
let settings = SettingsModel(defaults: defaults)
XCTAssertTrue(settings.onboardingCompleted)
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
}
func testShowDockIconPersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -26,6 +63,383 @@ final class SettingsModelTests: XCTestCase {
XCTAssertTrue(restored.showDockIcon)
}
func testHideFromScreenCapturePersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertFalse(settings.hideFromScreenCapture)
settings.hideFromScreenCapture = true
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.hideFromScreenCapture))
XCTAssertEqual(changes, [.hideFromScreenCapture])
let restored = SettingsModel(defaults: defaults)
XCTAssertTrue(restored.hideFromScreenCapture)
}
func testCompactModePersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertFalse(settings.compactMode)
settings.compactMode = true
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.compactMode))
XCTAssertEqual(changes, [.compactMode])
let restored = SettingsModel(defaults: defaults)
XCTAssertTrue(restored.compactMode)
}
func testPanelSidePersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertEqual(settings.panelLayout, .vertical)
XCTAssertEqual(settings.panelSide, .right)
settings.panelSide = .left
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.panelLayout), ClipboardPanelLayout.vertical.rawValue)
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.panelSide), ClipboardPanelSide.left.rawValue)
XCTAssertEqual(changes, [.panelLayout])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.panelLayout, .vertical)
XCTAssertEqual(restored.panelSide, .left)
}
func testImageCacheMinimumAllowsTwoMegabytes() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let twoMegabytes = Int64(2 * 1024 * 1024)
defaults.set(twoMegabytes, forKey: SettingsModel.Keys.imageCacheMaxBytes)
let settings = SettingsModel(defaults: defaults)
XCTAssertEqual(AppConfiguration.minCacheMaxBytes, twoMegabytes)
XCTAssertEqual(settings.imageCacheMaxBytes, twoMegabytes)
}
func testICloudSyncPersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertFalse(settings.iCloudSyncEnabled)
settings.setCloudSyncStatus(message: "Synced 3 clips to iCloud.")
XCTAssertEqual(settings.cloudSyncStatusMessage, "Synced 3 clips to iCloud.")
changes.removeAll()
settings.iCloudSyncEnabled = true
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.iCloudSyncEnabled))
XCTAssertEqual(settings.cloudSyncStatusMessage, "")
XCTAssertEqual(changes, [.cloudSync])
settings.setCloudSyncStatus(message: "Synced 3 clips to iCloud.")
changes.removeAll()
settings.iCloudSyncEnabled = false
XCTAssertFalse(defaults.bool(forKey: SettingsModel.Keys.iCloudSyncEnabled))
XCTAssertEqual(settings.cloudSyncStatusMessage, "")
XCTAssertEqual(changes, [.cloudSync])
let restored = SettingsModel(defaults: defaults)
XCTAssertFalse(restored.iCloudSyncEnabled)
}
func testPanelShelfHeightPersistsAndNotifies() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertEqual(settings.panelShelfHeight, 0)
settings.panelShelfHeight = 620
XCTAssertEqual(defaults.double(forKey: SettingsModel.Keys.panelShelfHeight), 620)
XCTAssertEqual(changes, [.panelSizing])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.panelShelfHeight, 620)
}
func testPauseCaptureUntilPersistsAndCanBeCleared() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
let pauseUntil = Date(timeIntervalSince1970: 1_234_567)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
settings.pauseCapture = true
settings.pauseCaptureUntil = pauseUntil
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.pauseCapture))
XCTAssertEqual(defaults.double(forKey: SettingsModel.Keys.pauseCaptureUntil), pauseUntil.timeIntervalSince1970)
XCTAssertEqual(changes, [.pauseCapture, .pauseCapture])
let restored = SettingsModel(defaults: defaults)
XCTAssertTrue(restored.pauseCapture)
XCTAssertEqual(restored.pauseCaptureUntil, pauseUntil)
restored.pauseCaptureUntil = nil
XCTAssertNil(defaults.object(forKey: SettingsModel.Keys.pauseCaptureUntil))
}
func testIgnoredAppsPersistsAndNotifiesNarrowChange() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
settings.ignoredApps = ["Safari", "Xcode"]
XCTAssertEqual(defaults.stringArray(forKey: SettingsModel.Keys.ignoredApps), ["Safari", "Xcode"])
XCTAssertEqual(changes, [.ignoredApps])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.ignoredApps, ["Safari", "Xcode"])
}
func testCommonControlSettingsNotifyNarrowChanges() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
settings.defaultSortMode = .links
settings.includeImageTextInSearch = true
settings.pruneDuplicates = false
settings.ignoredItemKindsRaw = [ClipboardItemKind.image.rawValue]
settings.keepFirstImage = false
settings.excludeSensitive = true
settings.clearHistoryOnQuit = true
XCTAssertEqual(changes, [
.defaultSortMode,
.includeImageTextInSearch,
.pruneDuplicates,
.ignoredItemKinds,
.keepFirstImage,
.excludeSensitive,
.clearHistoryOnQuit
])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.defaultSortMode, .links)
XCTAssertTrue(restored.includeImageTextInSearch)
XCTAssertFalse(restored.pruneDuplicates)
XCTAssertEqual(restored.ignoredItemKindsRaw, [ClipboardItemKind.image.rawValue])
XCTAssertFalse(restored.keepFirstImage)
XCTAssertTrue(restored.excludeSensitive)
XCTAssertTrue(restored.clearHistoryOnQuit)
}
func testIgnoredItemKindsCannotDisableEveryVisibleKind() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
let allVisibleKindRawValues = Self.visibleItemKinds.map(\.rawValue)
let expectedIgnoredKindRawValues = allVisibleKindRawValues.filter {
$0 != ClipboardItemKind.text.rawValue
}
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
settings.ignoredItemKindsRaw = allVisibleKindRawValues
XCTAssertEqual(settings.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
XCTAssertEqual(
defaults.object(forKey: SettingsModel.Keys.ignoredItemKinds) as? [Int],
expectedIgnoredKindRawValues
)
XCTAssertEqual(changes, [.ignoredItemKinds])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
}
func testStoredIgnoredItemKindsCannotDisableEveryVisibleKind() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let allVisibleKindRawValues = Self.visibleItemKinds.map(\.rawValue)
let expectedIgnoredKindRawValues = allVisibleKindRawValues.filter {
$0 != ClipboardItemKind.text.rawValue
}
defaults.set(allVisibleKindRawValues, forKey: SettingsModel.Keys.ignoredItemKinds)
let settings = SettingsModel(defaults: defaults)
XCTAssertEqual(settings.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
XCTAssertEqual(
defaults.object(forKey: SettingsModel.Keys.ignoredItemKinds) as? [Int],
expectedIgnoredKindRawValues
)
}
func testStoredImageCacheLimitIsClampedToSettingsRange() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let zeroDefaults = UserDefaults(suiteName: "\(suiteName).zero")!
let lowDefaults = UserDefaults(suiteName: "\(suiteName).low")!
let highDefaults = UserDefaults(suiteName: "\(suiteName).high")!
defer {
zeroDefaults.removePersistentDomain(forName: "\(suiteName).zero")
lowDefaults.removePersistentDomain(forName: "\(suiteName).low")
highDefaults.removePersistentDomain(forName: "\(suiteName).high")
}
zeroDefaults.set(0, forKey: SettingsModel.Keys.imageCacheMaxBytes)
let zeroSettings = SettingsModel(defaults: zeroDefaults)
XCTAssertEqual(zeroSettings.imageCacheMaxBytes, AppConfiguration.defaultCacheMaxBytes)
XCTAssertEqual(Int64(zeroDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.defaultCacheMaxBytes)
lowDefaults.set(1 * 1024 * 1024, forKey: SettingsModel.Keys.imageCacheMaxBytes)
let lowSettings = SettingsModel(defaults: lowDefaults)
XCTAssertEqual(lowSettings.imageCacheMaxBytes, AppConfiguration.minCacheMaxBytes)
XCTAssertEqual(Int64(lowDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.minCacheMaxBytes)
highDefaults.set(2048 * 1024 * 1024, forKey: SettingsModel.Keys.imageCacheMaxBytes)
let highSettings = SettingsModel(defaults: highDefaults)
XCTAssertEqual(highSettings.imageCacheMaxBytes, AppConfiguration.maxCacheMaxBytes)
XCTAssertEqual(Int64(highDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.maxCacheMaxBytes)
}
func testStoredHistoryLimitIsClampedToSettingsRange() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let zeroDefaults = UserDefaults(suiteName: "\(suiteName).zero")!
let lowDefaults = UserDefaults(suiteName: "\(suiteName).low")!
let highDefaults = UserDefaults(suiteName: "\(suiteName).high")!
defer {
zeroDefaults.removePersistentDomain(forName: "\(suiteName).zero")
lowDefaults.removePersistentDomain(forName: "\(suiteName).low")
highDefaults.removePersistentDomain(forName: "\(suiteName).high")
}
zeroDefaults.set(0, forKey: SettingsModel.Keys.maxHistoryItems)
let zeroSettings = SettingsModel(defaults: zeroDefaults)
XCTAssertEqual(zeroSettings.maxHistoryItems, AppConfiguration.defaultHistoryLength)
XCTAssertEqual(zeroDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.defaultHistoryLength)
lowDefaults.set(1, forKey: SettingsModel.Keys.maxHistoryItems)
let lowSettings = SettingsModel(defaults: lowDefaults)
XCTAssertEqual(lowSettings.maxHistoryItems, AppConfiguration.minHistoryLength)
XCTAssertEqual(lowDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.minHistoryLength)
highDefaults.set(100_000, forKey: SettingsModel.Keys.maxHistoryItems)
let highSettings = SettingsModel(defaults: highDefaults)
XCTAssertEqual(highSettings.maxHistoryItems, AppConfiguration.maxHistoryLength)
XCTAssertEqual(highDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.maxHistoryLength)
}
func testLimitAssignmentsAreClampedAndPersisted() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
settings.maxHistoryItems = 1
settings.maxHistoryItems = 100_000
settings.imageCacheMaxBytes = 1
settings.imageCacheMaxBytes = 2048 * 1024 * 1024
XCTAssertEqual(settings.maxHistoryItems, AppConfiguration.maxHistoryLength)
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.maxHistoryLength)
XCTAssertEqual(settings.imageCacheMaxBytes, AppConfiguration.maxCacheMaxBytes)
XCTAssertEqual(Int64(defaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.maxCacheMaxBytes)
XCTAssertEqual(changes, [
.maxHistoryItems,
.maxHistoryItems,
.imageCacheMaxBytes,
.imageCacheMaxBytes
])
}
func testHistoryRetentionDefaultsToOneMonthAndPersists() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer {
defaults.removePersistentDomain(forName: suiteName)
}
let settings = SettingsModel(defaults: defaults)
var changes: [SettingsModel.Change] = []
settings.observe { changes.append($0) }
XCTAssertEqual(settings.historyRetention, .oneMonth)
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.historyRetention), HistoryRetention.oneMonth.rawValue)
settings.historyRetention = .oneWeek
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.historyRetention), HistoryRetention.oneWeek.rawValue)
XCTAssertEqual(changes, [.historyRetention])
let restored = SettingsModel(defaults: defaults)
XCTAssertEqual(restored.historyRetention, .oneWeek)
}
func testCustomCollectionsPersistWithNormalizedColors() {
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -77,4 +491,17 @@ final class SettingsModelTests: XCTestCase {
XCTAssertEqual(restored.customCollectionNames, ["Product Research"])
XCTAssertEqual(restored.collectionColorHex(forCollectionNamed: "Product Research"), "#3366FF")
}
private static let visibleItemKinds: [ClipboardItemKind] = [
.text,
.code,
.url,
.image,
.color,
.audio,
.video,
.richText,
.pdf,
.file
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,13 @@ final class ShortcutManagerTests: XCTestCase {
manager.stop()
}
func testRejectsConfiguredShortcutConflictWithFixedStackCaptureShortcut() {
let manager = makeManager(openShortcut: ShortcutManager.stackCaptureShortcut)
XCTAssertEqual(manager.start(), .conflict(ShortcutManager.stackCaptureShortcut.displayText))
manager.stop()
}
private func makeManager(
openShortcut: ShortcutBinding,
settingsShortcut: ShortcutBinding = AppConfiguration.defaultSettingsShortcut