2026-06-30 02:25:52 -07:00
|
|
|
import XCTest
|
|
|
|
|
@testable import ClipBored
|
|
|
|
|
|
|
|
|
|
final class SettingsModelTests: XCTestCase {
|
2026-07-08 01:13:02 -04:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 02:25:52 -07:00
|
|
|
func testShowDockIconPersistsAndNotifies() {
|
|
|
|
|
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.showDockIcon)
|
|
|
|
|
|
|
|
|
|
settings.showDockIcon = true
|
|
|
|
|
|
|
|
|
|
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.showDockIcon))
|
|
|
|
|
XCTAssertEqual(changes.count, 1)
|
|
|
|
|
guard case .showDockIcon = changes.first else {
|
|
|
|
|
return XCTFail("Expected showDockIcon change notification")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let restored = SettingsModel(defaults: defaults)
|
|
|
|
|
XCTAssertTrue(restored.showDockIcon)
|
|
|
|
|
}
|
2026-06-30 03:51:06 -07:00
|
|
|
|
2026-07-08 01:13:02 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 03:51:06 -07:00
|
|
|
func testCustomCollectionsPersistWithNormalizedColors() {
|
|
|
|
|
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.ensureCollection(named: " Research Stack ", colorHex: "0a9eb8")
|
|
|
|
|
settings.ensureCollection(named: "research stack", colorHex: "#FF3355")
|
|
|
|
|
settings.ensureCollection(named: "Client Work", colorHex: "not-a-color")
|
|
|
|
|
|
|
|
|
|
XCTAssertEqual(settings.customCollectionNames, ["Research Stack", "Client Work"])
|
|
|
|
|
XCTAssertEqual(settings.collectionColorHex(forCollectionNamed: "research stack"), "#FF3355")
|
|
|
|
|
XCTAssertNil(settings.collectionColorHex(forCollectionNamed: "Client Work"))
|
|
|
|
|
XCTAssertEqual(changes, [.collections, .collections, .collections])
|
|
|
|
|
|
|
|
|
|
let restored = SettingsModel(defaults: defaults)
|
|
|
|
|
XCTAssertEqual(restored.customCollectionNames, ["Research Stack", "Client Work"])
|
|
|
|
|
XCTAssertEqual(restored.collectionColorHex(forCollectionNamed: "Research Stack"), "#FF3355")
|
|
|
|
|
}
|
2026-06-30 04:01:57 -07:00
|
|
|
|
|
|
|
|
func testCustomCollectionsCanBeUpdatedAndDeleted() {
|
|
|
|
|
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
|
|
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
|
|
|
defer {
|
|
|
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
|
|
|
}
|
|
|
|
|
let settings = SettingsModel(defaults: defaults)
|
|
|
|
|
|
|
|
|
|
settings.ensureCollection(named: "Research Stack", colorHex: "#0A9EB8")
|
|
|
|
|
settings.ensureCollection(named: "Client Work", colorHex: "#FF3355")
|
|
|
|
|
|
|
|
|
|
let updatedName = settings.updateCollection(named: "research stack", to: "Product Research", colorHex: "#3366FF")
|
|
|
|
|
|
|
|
|
|
XCTAssertEqual(updatedName, "Product Research")
|
|
|
|
|
XCTAssertEqual(settings.customCollectionNames, ["Product Research", "Client Work"])
|
|
|
|
|
XCTAssertNil(settings.collectionColorHex(forCollectionNamed: "Research Stack"))
|
|
|
|
|
XCTAssertEqual(settings.collectionColorHex(forCollectionNamed: "Product Research"), "#3366FF")
|
|
|
|
|
|
|
|
|
|
settings.deleteCollection(named: "client work")
|
|
|
|
|
|
|
|
|
|
XCTAssertEqual(settings.customCollectionNames, ["Product Research"])
|
|
|
|
|
XCTAssertNil(settings.collectionColorHex(forCollectionNamed: "Client Work"))
|
|
|
|
|
|
|
|
|
|
let restored = SettingsModel(defaults: defaults)
|
|
|
|
|
XCTAssertEqual(restored.customCollectionNames, ["Product Research"])
|
|
|
|
|
XCTAssertEqual(restored.collectionColorHex(forCollectionNamed: "Product Research"), "#3366FF")
|
|
|
|
|
}
|
2026-07-08 01:13:02 -04:00
|
|
|
|
|
|
|
|
private static let visibleItemKinds: [ClipboardItemKind] = [
|
|
|
|
|
.text,
|
|
|
|
|
.code,
|
|
|
|
|
.url,
|
|
|
|
|
.image,
|
|
|
|
|
.color,
|
|
|
|
|
.audio,
|
|
|
|
|
.video,
|
|
|
|
|
.richText,
|
|
|
|
|
.pdf,
|
|
|
|
|
.file
|
|
|
|
|
]
|
2026-06-30 02:25:52 -07:00
|
|
|
}
|