This commit is contained in:
@@ -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() {
|
||||
|
||||
144
tests/clipboredtests/ClipboardCloudSyncServiceTests.swift
Normal file
144
tests/clipboredtests/ClipboardCloudSyncServiceTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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) })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
81
tests/clipboredtests/LinkPreviewWindowControllerTests.swift
Normal file
81
tests/clipboredtests/LinkPreviewWindowControllerTests.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
75
tests/clipboredtests/OnboardingWindowControllerTests.swift
Normal file
75
tests/clipboredtests/OnboardingWindowControllerTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
|
||||
1064
tests/clipboredtests/SettingsWindowControllerTests.swift
Normal file
1064
tests/clipboredtests/SettingsWindowControllerTests.swift
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user