import AppKit import XCTest @testable import ClipBored final class SettingsWindowControllerTests: XCTestCase { private var tempURLs: [URL] = [] private var defaultsSuites: [String] = [] override func tearDown() { tempURLs.forEach { try? FileManager.default.removeItem(at: $0) } for suite in defaultsSuites { UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite) } tempURLs.removeAll() defaultsSuites.removeAll() super.tearDown() } func testSettingsWindowIsResizableWithPracticalMinimumSize() { let (controller, _) = makeController() XCTAssertTrue(controller.debugWindowStyleMask.contains(.resizable)) XCTAssertEqual(controller.debugWindowMinSize.width, 620, accuracy: 0.5) XCTAssertEqual(controller.debugWindowMinSize.height, 560, accuracy: 0.5) XCTAssertGreaterThanOrEqual(controller.debugWindowContentSize.width, 700) XCTAssertGreaterThanOrEqual(controller.debugWindowContentSize.height, 600) } func testSettingsTabsUsePinnedScrollableDocuments() { let (controller, _) = makeController() let metrics = controller.debugSettingsTabLayoutMetrics XCTAssertEqual(metrics.map { $0.label }, ["General", "Shortcuts", "Capture", "Privacy", "Performance", "Data"]) for metric in metrics { XCTAssertGreaterThan(metric.viewport.width, 0, metric.label) XCTAssertGreaterThan(metric.viewport.height, 0, metric.label) XCTAssertEqual(metric.document.width, metric.viewport.width, accuracy: 1, metric.label) XCTAssertGreaterThan(metric.document.height, 0, metric.label) XCTAssertFalse(metric.hasHorizontalScroller, metric.label) } } func testSettingsDataTabKeepsExtraNativeHighlightPadding() { let (controller, _) = makeController() XCTAssertEqual(controller.debugSettingsTabLayoutMetrics.map { $0.label }, ["General", "Shortcuts", "Capture", "Privacy", "Performance", "Data"]) XCTAssertEqual(controller.debugRawSettingsTabLabels.last?.clipboardTrimmed, "Data") XCTAssertGreaterThan(controller.debugRawSettingsTabLabels.last?.count ?? 0, "Data".count) } func testSettingsTabsKeepContentAnchoredToTopLeadingCorner() { let (controller, _) = makeController() let metrics = controller.debugSettingsTabContentPlacementMetrics XCTAssertEqual(metrics.map { $0.label }, ["General", "Shortcuts", "Capture", "Privacy", "Performance", "Data"]) for metric in metrics { XCTAssertGreaterThan(metric.contentBounds.width, 0, metric.label) XCTAssertGreaterThan(metric.contentBounds.height, 0, metric.label) XCTAssertLessThanOrEqual(metric.contentBounds.minX, 40, metric.label) XCTAssertLessThanOrEqual(metric.contentBounds.minY, 40, metric.label) XCTAssertLessThanOrEqual(metric.contentBounds.maxX, metric.document.width + 1, metric.label) } } func testSettingsTabsAvoidHorizontalOverflowAtMinimumWindowSize() { let (controller, _) = makeController() controller.debugSetWindowContentSize(NSSize(width: 620, height: 560)) let metrics = controller.debugSettingsTabLayoutAuditMetrics XCTAssertEqual(metrics.map { $0.label }, ["General", "Shortcuts", "Capture", "Privacy", "Performance", "Data"]) for metric in metrics { XCTAssertEqual(metric.overflowingViewCount, 0, metric.label) XCTAssertEqual(metric.zeroSizedControlCount, 0, metric.label) } } func testCloudSyncStatusIsCachedAcrossUnrelatedSettingsRefreshes() { let cloudSyncService = CloudSyncProbe( status: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) let (controller, settings) = makeController(cloudSyncService: cloudSyncService) XCTAssertEqual(controller.debugCloudSyncStatusText, "iCloud Sync is off.") XCTAssertEqual(controller.debugCloudSyncActionButtonsAreEnabled, [false, false, false]) XCTAssertEqual(cloudSyncService.statusCallCount, 0) settings.iCloudSyncEnabled = true drainMainQueue() XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud ready") XCTAssertEqual(controller.debugCloudSyncActionButtonsAreEnabled, [true, true, true]) settings.maxHistoryItems += 25 settings.setPasteStatus(message: "Pasted latest clip") drainMainQueue() XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud ready") settings.setCloudSyncStatus(message: "Synced 3 clips and 1 attachment to iCloud.") drainMainQueue() XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugCloudSyncStatusText, "Synced 3 clips and 1 attachment to iCloud.") settings.setCloudSyncStatus(message: "") drainMainQueue() XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud ready") } func testTurningCloudSyncOffClearsStaleActionStatus() { let cloudSyncService = CloudSyncProbe( status: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) let (controller, settings) = makeController(cloudSyncService: cloudSyncService) let initialFullRefreshCount = controller.debugFullRefreshCount controller.debugSetICloudSyncEnabled(true) drainMainQueue() XCTAssertTrue(settings.iCloudSyncEnabled) XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud ready") XCTAssertEqual(cloudSyncService.statusCallCount, 1) settings.setCloudSyncStatus(message: "Synced 3 clips and 1 attachment to iCloud.") drainMainQueue() XCTAssertEqual(controller.debugCloudSyncStatusText, "Synced 3 clips and 1 attachment to iCloud.") controller.debugSetICloudSyncEnabled(false) drainMainQueue() XCTAssertFalse(settings.iCloudSyncEnabled) XCTAssertEqual(settings.cloudSyncStatusMessage, "") XCTAssertEqual(controller.debugCloudSyncStatusText, "iCloud Sync is off.") XCTAssertEqual(controller.debugCloudSyncActionButtonsAreEnabled, [false, false, false]) XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testCloudSyncActionsStayDisabledWhenSyncIsUnavailable() { let cloudSyncService = CloudSyncProbe( status: ClipboardCloudSyncStatus( isAvailable: false, archiveURL: nil, lastModifiedAt: nil, message: "Probe iCloud unavailable" ) ) let (controller, settings) = makeController(cloudSyncService: cloudSyncService) let initialFullRefreshCount = controller.debugFullRefreshCount XCTAssertEqual(controller.debugCloudSyncActionButtonsAreEnabled, [false, false, false]) settings.iCloudSyncEnabled = true drainMainQueue() XCTAssertTrue(settings.iCloudSyncEnabled) XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud unavailable") XCTAssertEqual(controller.debugCloudSyncActionButtonsAreEnabled, [false, false, false]) XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testCloudSyncStatusPresentationUsesErrorColorForFailedActionMessages() { let failed = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "iCloud Sync failed: No ClipBored iCloud archive has been created yet.", isSyncEnabled: true, cloudStatus: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) XCTAssertEqual(failed.message, "iCloud Sync failed: No ClipBored iCloud archive has been created yet.") XCTAssertEqual(failed.textColor, .systemRed) let unavailable = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "", isSyncEnabled: true, cloudStatus: ClipboardCloudSyncStatus( isAvailable: false, archiveURL: nil, lastModifiedAt: nil, message: "Probe iCloud unavailable" ) ) XCTAssertEqual(unavailable.message, "Probe iCloud unavailable") XCTAssertEqual(unavailable.textColor, .systemOrange) let synced = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "Synced 3 clips and 1 attachment to iCloud.", isSyncEnabled: true, cloudStatus: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) XCTAssertEqual(synced.message, "Synced 3 clips and 1 attachment to iCloud.") XCTAssertEqual(synced.textColor, .systemGreen) let opened = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "Opened iCloud sync location.", isSyncEnabled: true, cloudStatus: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) XCTAssertEqual(opened.message, "Opened iCloud sync location.") XCTAssertEqual(opened.textColor, .systemGreen) let restoredWithSkips = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "Restored 3 clips and 1 attachment from iCloud. Skipped 1 clip and 0 attachments.", isSyncEnabled: true, cloudStatus: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) XCTAssertEqual( restoredWithSkips.message, "Restored 3 clips and 1 attachment from iCloud. Skipped 1 clip and 0 attachments." ) XCTAssertEqual(restoredWithSkips.textColor, .systemOrange) let off = SettingsWindowController.cloudSyncStatusPresentation( storedStatus: "", isSyncEnabled: false, cloudStatus: nil ) XCTAssertEqual(off.message, "iCloud Sync is off.") XCTAssertEqual(off.textColor, .secondaryLabelColor) } func testExternalCloudSyncDisableShowsOffStateInsteadOfStaleStatus() { let cloudSyncService = CloudSyncProbe( status: ClipboardCloudSyncStatus( isAvailable: true, archiveURL: makeTempDirectory().appendingPathComponent("ClipBored.clipboredarchive"), lastModifiedAt: nil, message: "Probe iCloud ready" ) ) let (controller, settings) = makeController(cloudSyncService: cloudSyncService) let initialFullRefreshCount = controller.debugFullRefreshCount settings.iCloudSyncEnabled = true drainMainQueue() XCTAssertEqual(controller.debugCloudSyncStatusText, "Probe iCloud ready") XCTAssertEqual(cloudSyncService.statusCallCount, 1) settings.setCloudSyncStatus(message: "Synced 3 clips and 1 attachment to iCloud.") drainMainQueue() XCTAssertEqual(controller.debugCloudSyncStatusText, "Synced 3 clips and 1 attachment to iCloud.") settings.iCloudSyncEnabled = false drainMainQueue() XCTAssertEqual(settings.cloudSyncStatusMessage, "") XCTAssertEqual(controller.debugCloudSyncStatusText, "iCloud Sync is off.") XCTAssertEqual(cloudSyncService.statusCallCount, 1) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testIgnoredAppsEditorPreservesDraftTextWhileFocused() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount settings.ignoredApps = ["Safari", "Xcode"] drainMainQueue() XCTAssertEqual(controller.debugIgnoredAppsText, "Safari, Xcode") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertGreaterThan(controller.debugIgnoredAppsRefreshCount, 0) XCTAssertTrue(controller.debugFocusIgnoredAppsEditor()) XCTAssertTrue(controller.debugIgnoredAppsEditorIsFocused) let focusedFullRefreshCount = controller.debugFullRefreshCount let focusedIgnoredAppsRefreshCount = controller.debugIgnoredAppsRefreshCount controller.debugSetIgnoredAppsText("Safari,\nXcode, Terminal, ") drainMainQueue() XCTAssertEqual(settings.ignoredApps, ["Safari", "Xcode"]) XCTAssertEqual(controller.debugIgnoredAppsText, "Safari,\nXcode, Terminal, ") XCTAssertEqual(controller.debugFullRefreshCount, focusedFullRefreshCount) XCTAssertEqual(controller.debugIgnoredAppsRefreshCount, focusedIgnoredAppsRefreshCount) settings.maxHistoryItems += 25 settings.setPasteStatus(message: "Pasted latest clip") drainMainQueue() XCTAssertEqual(controller.debugIgnoredAppsText, "Safari,\nXcode, Terminal, ") controller.debugEndIgnoredAppsEditing() drainMainQueue() XCTAssertEqual(settings.ignoredApps, ["Safari", "Xcode", "Terminal"]) XCTAssertEqual(controller.debugIgnoredAppsText, "Safari, Xcode, Terminal") } func testFocusedIgnoredAppsEditorWithoutDraftDoesNotOverwriteExternalChanges() { let (controller, settings) = makeController() settings.ignoredApps = ["Safari"] drainMainQueue() XCTAssertEqual(controller.debugIgnoredAppsText, "Safari") XCTAssertTrue(controller.debugFocusIgnoredAppsEditor()) settings.ignoredApps = ["Xcode", "Terminal"] drainMainQueue() XCTAssertEqual(controller.debugIgnoredAppsText, "Safari") XCTAssertEqual(settings.ignoredApps, ["Xcode", "Terminal"]) controller.debugEndIgnoredAppsEditing() drainMainQueue() XCTAssertEqual(controller.debugIgnoredAppsText, "Xcode, Terminal") XCTAssertEqual(settings.ignoredApps, ["Xcode", "Terminal"]) } func testClosingSettingsWindowCommitsFocusedIgnoredAppsDraft() { let (controller, settings) = makeController() settings.ignoredApps = ["Safari"] drainMainQueue() XCTAssertTrue(controller.debugFocusIgnoredAppsEditor()) controller.debugSetIgnoredAppsText("Safari, Xcode, Terminal") drainMainQueue() XCTAssertEqual(settings.ignoredApps, ["Safari"]) XCTAssertEqual(controller.debugIgnoredAppsText, "Safari, Xcode, Terminal") controller.debugCloseWindow() drainMainQueue() XCTAssertEqual(settings.ignoredApps, ["Safari", "Xcode", "Terminal"]) XCTAssertEqual(controller.debugIgnoredAppsText, "Safari, Xcode, Terminal") } func testNarrowSettingsChangesAvoidFullWindowRefresh() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount settings.maxHistoryItems = 75 drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertEqual(controller.debugHistoryText, "History length: 75") settings.imageCacheMaxBytes = 128 * 1024 * 1024 drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertEqual(controller.debugCacheStatusText, "Current cache cap: 128 MB") settings.setPasteStatus(message: "Pasted latest clip") drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertEqual(controller.debugPasteStatusText, "Pasted latest clip") } func testLimitControlsRefreshToClampedStoredValues() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount let minCacheMegabytes = Int(AppConfiguration.minCacheMaxBytes / 1024 / 1024) let maxCacheMegabytes = Int(AppConfiguration.maxCacheMaxBytes / 1024 / 1024) controller.debugSetHistoryStepperValue(AppConfiguration.minHistoryLength - 25) drainMainQueue() XCTAssertEqual(settings.maxHistoryItems, AppConfiguration.minHistoryLength) XCTAssertEqual(controller.debugHistoryText, "History length: \(AppConfiguration.minHistoryLength)") XCTAssertEqual(controller.debugHistoryStepperValue, AppConfiguration.minHistoryLength) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetHistoryStepperValue(AppConfiguration.maxHistoryLength + 25) drainMainQueue() XCTAssertEqual(settings.maxHistoryItems, AppConfiguration.maxHistoryLength) XCTAssertEqual(controller.debugHistoryText, "History length: \(AppConfiguration.maxHistoryLength)") XCTAssertEqual(controller.debugHistoryStepperValue, AppConfiguration.maxHistoryLength) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetCacheSliderMegabytes(minCacheMegabytes - 2) drainMainQueue() XCTAssertEqual(settings.imageCacheMaxBytes, AppConfiguration.minCacheMaxBytes) XCTAssertEqual(controller.debugCacheStatusText, "Current cache cap: \(minCacheMegabytes) MB") XCTAssertEqual(controller.debugCacheSliderMegabytes, minCacheMegabytes) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetCacheSliderMegabytes(maxCacheMegabytes + 128) drainMainQueue() XCTAssertEqual(settings.imageCacheMaxBytes, AppConfiguration.maxCacheMaxBytes) XCTAssertEqual(controller.debugCacheStatusText, "Current cache cap: \(maxCacheMegabytes) MB") XCTAssertEqual(controller.debugCacheSliderMegabytes, maxCacheMegabytes) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testStatusLabelsWrapLongMessagesInsteadOfTruncating() { let (controller, settings) = makeController() let longMessage = "Copied 12 selected clips as Text. Grant Accessibility access in System Settings to paste automatically into the previous application." settings.setPasteStatus(message: longMessage) drainMainQueue() XCTAssertEqual(controller.debugPasteStatusText, longMessage) XCTAssertTrue(controller.debugStatusLabelsAllowWrapping) } func testPasteStatusPresentationUsesSuccessWarningAndErrorColors() { let idle = SettingsWindowController.pasteStatusPresentation(storedStatus: "") XCTAssertEqual(idle.message, "No paste action yet.") XCTAssertEqual(idle.textColor, .secondaryLabelColor) let pasted = SettingsWindowController.pasteStatusPresentation(storedStatus: "Pasted") XCTAssertEqual(pasted.message, "Pasted") XCTAssertEqual(pasted.textColor, .systemGreen) let copiedNeedsPermission = SettingsWindowController.pasteStatusPresentation( storedStatus: "Copied. Grant Accessibility access to paste automatically." ) XCTAssertEqual(copiedNeedsPermission.message, "Copied. Grant Accessibility access to paste automatically.") XCTAssertEqual(copiedNeedsPermission.textColor, .systemOrange) let failed = SettingsWindowController.pasteStatusPresentation( storedStatus: "Could not write item to clipboard." ) XCTAssertEqual(failed.message, "Could not write item to clipboard.") XCTAssertEqual(failed.textColor, .systemRed) } func testDataStatusPresentationUsesSuccessWarningAndErrorColors() { let idle = SettingsWindowController.dataStatusPresentation(storedStatus: "") XCTAssertEqual(idle.message, "") XCTAssertEqual(idle.textColor, .secondaryLabelColor) let exported = SettingsWindowController.dataStatusPresentation( storedStatus: "Exported 3 clips and 1 attachments." ) XCTAssertEqual(exported.message, "Exported 3 clips and 1 attachments.") XCTAssertEqual(exported.textColor, .systemGreen) let cleared = SettingsWindowController.dataStatusPresentation( storedStatus: "Cleared thumbnail cache." ) XCTAssertEqual(cleared.message, "Cleared thumbnail cache.") XCTAssertEqual(cleared.textColor, .systemGreen) let partialImport = SettingsWindowController.dataStatusPresentation( storedStatus: "Imported 3 clips and 1 attachments. Skipped 1 clips and 0 attachments." ) XCTAssertEqual( partialImport.message, "Imported 3 clips and 1 attachments. Skipped 1 clips and 0 attachments." ) XCTAssertEqual(partialImport.textColor, .systemOrange) let failed = SettingsWindowController.dataStatusPresentation( storedStatus: "The archive couldn't be opened." ) XCTAssertEqual(failed.message, "The archive couldn't be opened.") XCTAssertEqual(failed.textColor, .systemRed) } func testDestructiveDataActionsShowSuccessStatus() { let fixture = makeControllerFixture() fixture.store.upsert(makeItem("saved clip", displayText: "Saved Clip", created: Date())) XCTAssertEqual(fixture.store.items.count, 1) XCTAssertEqual(fixture.controller.debugDataStatusText, "") XCTAssertEqual(fixture.controller.debugDataStatusSectionTitle, "Data") fixture.controller.debugSetDestructiveActionConfirmation(true) fixture.controller.debugClearClipboardHistory() XCTAssertTrue(fixture.store.items.isEmpty) XCTAssertEqual(fixture.controller.debugDataStatusText, "Cleared clipboard history.") XCTAssertEqual(fixture.controller.debugDataStatusColor, .systemGreen) fixture.controller.debugClearThumbnailCache() XCTAssertEqual(fixture.controller.debugDataStatusText, "Cleared thumbnail cache.") XCTAssertEqual(fixture.controller.debugDataStatusColor, .systemGreen) } func testCaptureStatusPresentationUsesReadyWarningAndErrorColors() { let idle = SettingsWindowController.captureStatusPresentation(storedStatus: "") XCTAssertEqual(idle.message, "Capture status will appear after the app sees clipboard activity.") XCTAssertEqual(idle.textColor, .secondaryLabelColor) let captured = SettingsWindowController.captureStatusPresentation( storedStatus: "Captured text from Safari." ) XCTAssertEqual(captured.message, "Captured text from Safari.") XCTAssertEqual(captured.textColor, .systemGreen) let skipped = SettingsWindowController.captureStatusPresentation( storedStatus: "Skipped: Audio items are ignored in capture settings." ) XCTAssertEqual(skipped.message, "Skipped: Audio items are ignored in capture settings.") XCTAssertEqual(skipped.textColor, .systemOrange) let validationWarning = SettingsWindowController.captureStatusPresentation( storedStatus: "At least one content type must stay enabled." ) XCTAssertEqual(validationWarning.message, "At least one content type must stay enabled.") XCTAssertEqual(validationWarning.textColor, .systemOrange) let error = SettingsWindowController.captureStatusPresentation( storedStatus: "Error: Clipboard read failed." ) XCTAssertEqual(error.message, "Error: Clipboard read failed.") XCTAssertEqual(error.textColor, .systemRed) } func testAccessibilityPermissionRefreshAvoidsFullWindowRefresh() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount settings.setAccessibilityPermissionStatus(message: "Accessibility permission not granted.") drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertEqual(controller.debugAccessibilityStatusText, "Accessibility permission not granted.") controller.debugRefreshAccessibilityPermissionStatus() drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertFalse(controller.debugAccessibilityStatusText.isEmpty) } func testStoredAccessibilityWarningUsesWarningColorEvenWhenPermissionIsGranted() { let warning = SettingsWindowController.accessibilityPermissionStatusPresentation( storedStatus: "Accessibility permission not granted.", isTrusted: true ) XCTAssertEqual(warning.message, "Accessibility permission not granted.") XCTAssertEqual(warning.textColor, .systemOrange) let granted = SettingsWindowController.accessibilityPermissionStatusPresentation( storedStatus: "", isTrusted: true ) XCTAssertEqual(granted.message, "Granted") XCTAssertEqual(granted.textColor, .systemGreen) } func testCommonControlSettingsAvoidFullWindowRefresh() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount settings.defaultSortMode = .links settings.includeImageTextInSearch = true settings.pruneDuplicates = false settings.ignoredItemKindsRaw = [ClipboardItemKind.image.rawValue] settings.keepFirstImage = false settings.excludeSensitive = true settings.clearHistoryOnQuit = true drainMainQueue() XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) XCTAssertEqual(controller.debugDefaultSortTitle, ClipboardSortMode.links.title) XCTAssertTrue(controller.debugIncludeImageTextIsEnabled) XCTAssertFalse(controller.debugPruneDuplicatesIsEnabled) XCTAssertFalse(controller.debugAllowedKindIsEnabled(.image)) XCTAssertTrue(controller.debugAllowedKindIsEnabled(.text)) XCTAssertFalse(controller.debugKeepFirstImageIsEnabled) XCTAssertTrue(controller.debugExcludeSensitiveIsEnabled) XCTAssertTrue(controller.debugClearHistoryOnQuitIsEnabled) } func testAllowedContentTypesKeepAtLeastOneKindEnabled() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount let kindsToDisable: [ClipboardItemKind] = [ .code, .url, .image, .color, .audio, .video, .richText, .pdf, .file ] settings.setCaptureStatus(message: "Captured text from Safari.") drainMainQueue() XCTAssertEqual(controller.debugCaptureStatusColor, .systemGreen) for kind in kindsToDisable { controller.debugSetAllowedKindEnabled(kind, false) drainMainQueue() XCTAssertFalse(controller.debugAllowedKindIsEnabled(kind)) } XCTAssertTrue(controller.debugAllowedKindIsEnabled(.text)) XCTAssertFalse(settings.ignoredItemKindsRaw.contains(ClipboardItemKind.text.rawValue)) controller.debugSetAllowedKindEnabled(.text, false) drainMainQueue() XCTAssertTrue(controller.debugAllowedKindIsEnabled(.text)) XCTAssertFalse(settings.ignoredItemKindsRaw.contains(ClipboardItemKind.text.rawValue)) XCTAssertEqual(controller.debugCaptureStatusText, "At least one content type must stay enabled.") XCTAssertEqual(controller.debugCaptureStatusColor, .systemOrange) XCTAssertEqual(settings.captureStatusMessage, "At least one content type must stay enabled.") settings.setPasteStatus(message: "Pasted latest clip") drainMainQueue() XCTAssertEqual(controller.debugCaptureStatusText, "At least one content type must stay enabled.") XCTAssertEqual(controller.debugCaptureStatusColor, .systemOrange) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetAllowedKindEnabled(.image, true) drainMainQueue() XCTAssertTrue(controller.debugAllowedKindIsEnabled(.image)) XCTAssertEqual(settings.captureStatusMessage, "Allowed content types updated.") XCTAssertEqual(controller.debugCaptureStatusText, "Allowed content types updated.") XCTAssertEqual(controller.debugCaptureStatusColor, .systemGreen) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testExternalAllowedContentTypeChangesKeepAtLeastOneKindEnabled() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount let allVisibleKindRawValues = Self.visibleItemKinds.map(\.rawValue) settings.ignoredItemKindsRaw = allVisibleKindRawValues drainMainQueue() XCTAssertTrue(controller.debugAllowedKindIsEnabled(.text)) XCTAssertFalse(settings.ignoredItemKindsRaw.contains(ClipboardItemKind.text.rawValue)) for kind in Self.visibleItemKinds where kind != .text { XCTAssertFalse(controller.debugAllowedKindIsEnabled(kind)) } XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testShortcutEditsAvoidFullWindowRefreshAndRecoverEmptyKey() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount controller.debugCommitOpenShortcutKeyText("k") drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "k") XCTAssertEqual(controller.debugOpenShortcutKeyText, "K") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugCommitOpenShortcutKeyText("space") drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "k") XCTAssertEqual(controller.debugOpenShortcutKeyText, "K") XCTAssertEqual(controller.debugShortcutStatusText, "Unsupported shortcut: ⌘⌥SPACE") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugCommitOpenShortcutKeyText("⌘") drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "k") XCTAssertEqual(controller.debugOpenShortcutKeyText, "K") XCTAssertEqual(controller.debugShortcutStatusText, "Unsupported shortcut: ⌘") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugCommitOpenShortcutKeyText("") drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "k") XCTAssertEqual(controller.debugOpenShortcutKeyText, "K") XCTAssertEqual(controller.debugShortcutStatusText, "Registered") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testShortcutStatusPresentationUsesPositiveAndErrorColors() { let registered = SettingsWindowController.shortcutStatusPresentation(storedStatus: "") XCTAssertEqual(registered.message, "Registered") XCTAssertEqual(registered.textColor, .systemGreen) let unsupported = SettingsWindowController.shortcutStatusPresentation( storedStatus: "Unsupported shortcut: ⇧V" ) XCTAssertEqual(unsupported.message, "Unsupported shortcut: ⇧V") XCTAssertEqual(unsupported.textColor, .systemRed) let conflict = SettingsWindowController.shortcutStatusPresentation( storedStatus: "Shortcut is already in use: ⌘⇧C" ) XCTAssertEqual(conflict.message, "Shortcut is already in use: ⌘⇧C") XCTAssertEqual(conflict.textColor, .systemRed) } func testShortcutDraftSurvivesExternalShortcutRefreshWhileEditing() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount XCTAssertTrue(controller.debugBeginOpenShortcutKeyEditing()) controller.debugSetOpenShortcutKeyDraft("z") settings.openShortcut = ShortcutBinding( key: "j", modifierFlags: NSEvent.ModifierFlags.command.rawValue ) drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "j") XCTAssertEqual(controller.debugOpenShortcutKeyText, "z") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugEndOpenShortcutKeyEditing() drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "z") XCTAssertEqual(controller.debugOpenShortcutKeyText, "Z") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testClosingSettingsWindowCommitsShortcutDraft() { let (controller, settings) = makeController() let initialShortcut = settings.openShortcut XCTAssertTrue(controller.debugBeginOpenShortcutKeyEditing()) controller.debugSetOpenShortcutKeyDraft("b") drainMainQueue() XCTAssertEqual(settings.openShortcut, initialShortcut) XCTAssertEqual(controller.debugOpenShortcutKeyText, "b") controller.debugCloseWindow() drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "b") XCTAssertEqual(settings.openShortcut.modifierFlags, initialShortcut.modifierFlags) XCTAssertEqual(controller.debugOpenShortcutKeyText, "B") } func testShortcutModifierEditsRejectUnsupportedBindingsWithoutSaving() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount let initialShortcut = settings.openShortcut controller.debugSetOpenShortcutModifiers(command: false, option: false, control: false, shift: false) drainMainQueue() XCTAssertEqual(settings.openShortcut, initialShortcut) XCTAssertEqual(controller.debugOpenShortcutKeyText, initialShortcut.key.uppercased()) XCTAssertEqual(controller.debugShortcutStatusText, "Unsupported shortcut: V") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetOpenShortcutModifiers(command: false, option: false, control: false, shift: true) drainMainQueue() XCTAssertEqual(settings.openShortcut, initialShortcut) XCTAssertEqual(controller.debugShortcutStatusText, "Unsupported shortcut: ⇧V") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetOpenShortcutModifiers(command: false, option: false, control: true, shift: false) drainMainQueue() XCTAssertEqual(settings.openShortcut.key, initialShortcut.key) XCTAssertEqual(settings.openShortcut.modifierFlags, NSEvent.ModifierFlags.control.rawValue) XCTAssertEqual(controller.debugShortcutStatusText, "Registered") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testShortcutKeyFieldExplicitModifiersReplaceExistingModifierState() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount XCTAssertEqual(settings.openShortcut.displayText, "⌘⌥V") controller.debugCommitOpenShortcutKeyText("⌘K") drainMainQueue() XCTAssertEqual(settings.openShortcut.key, "k") XCTAssertEqual(settings.openShortcut.modifierFlags, NSEvent.ModifierFlags.command.rawValue) XCTAssertEqual(settings.openShortcut.displayText, "⌘K") XCTAssertEqual(controller.debugOpenShortcutKeyText, "K") XCTAssertEqual(controller.debugShortcutStatusText, "Registered") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testShortcutEditsRejectConflictingBindingsWithoutSaving() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount controller.debugCommitOpenShortcutKeyText(",") drainMainQueue() let commandOptionComma = settings.openShortcut controller.debugSetOpenShortcutModifiers(command: true, option: false, control: false, shift: false) drainMainQueue() XCTAssertEqual(settings.openShortcut, commandOptionComma) XCTAssertEqual(controller.debugShortcutStatusText, "Shortcut is already in use: ⌘,") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugCommitOpenShortcutKeyText("space") drainMainQueue() XCTAssertEqual(settings.openShortcut, commandOptionComma) XCTAssertEqual(controller.debugOpenShortcutKeyText, ",") XCTAssertEqual(controller.debugShortcutStatusText, "Unsupported shortcut: ⌘⌥SPACE") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugCommitOpenShortcutKeyText("c") drainMainQueue() let commandOptionC = settings.openShortcut controller.debugSetOpenShortcutModifiers(command: true, option: false, control: false, shift: true) drainMainQueue() XCTAssertEqual(settings.openShortcut, commandOptionC) XCTAssertEqual(controller.debugShortcutStatusText, "Shortcut is already in use: ⌘⇧C") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testLaunchAtLoginChangesAvoidFullWindowRefresh() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount settings.launchAtLogin = true drainMainQueue() XCTAssertTrue(controller.debugLaunchAtLoginIsEnabled) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) settings.setLaunchAtLoginStatus(message: "Launch agent unavailable") drainMainQueue() XCTAssertEqual(controller.debugLaunchStatusText, "Launch agent unavailable") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetLaunchAtLoginEnabled(false) drainMainQueue(iterations: 40) XCTAssertFalse(settings.launchAtLogin) XCTAssertEqual(settings.launchAtLoginErrorMessage, "") XCTAssertFalse(controller.debugLaunchAtLoginIsEnabled) XCTAssertEqual(controller.debugLaunchStatusText, "") XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } func testLaunchAtLoginStatusPresentationUsesErrorColorForFailures() { let idle = SettingsWindowController.launchAtLoginStatusPresentation(storedStatus: "") XCTAssertEqual(idle.message, "") XCTAssertEqual(idle.textColor, .secondaryLabelColor) let failure = SettingsWindowController.launchAtLoginStatusPresentation( storedStatus: "Launch-at-login failed: Service unavailable" ) XCTAssertEqual(failure.message, "Launch-at-login failed: Service unavailable") XCTAssertEqual(failure.textColor, .systemRed) } func testVisibilityControlsKeepOneSurfaceEnabledWithoutFullRefresh() { let (controller, settings) = makeController() let initialFullRefreshCount = controller.debugFullRefreshCount XCTAssertTrue(controller.debugShowMenuBarIconIsEnabled) XCTAssertFalse(controller.debugShowDockIconIsEnabled) controller.debugSetShowMenuBarIconEnabled(false) XCTAssertFalse(settings.showMenuBarIcon) XCTAssertTrue(settings.showDockIcon) XCTAssertFalse(controller.debugShowMenuBarIconIsEnabled) XCTAssertTrue(controller.debugShowDockIconIsEnabled) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) drainMainQueue() XCTAssertFalse(controller.debugShowMenuBarIconIsEnabled) XCTAssertTrue(controller.debugShowDockIconIsEnabled) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) controller.debugSetShowDockIconEnabled(false) XCTAssertTrue(settings.showMenuBarIcon) XCTAssertFalse(settings.showDockIcon) XCTAssertTrue(controller.debugShowMenuBarIconIsEnabled) XCTAssertFalse(controller.debugShowDockIconIsEnabled) XCTAssertEqual(controller.debugFullRefreshCount, initialFullRefreshCount) } private struct ControllerFixture { let controller: SettingsWindowController let settings: SettingsModel let store: ClipboardStore } private func makeControllerFixture( cloudSyncService: ClipboardCloudSyncServicing = CloudSyncProbe() ) -> ControllerFixture { let suiteName = "com.clipbored.settingswindow.\(UUID().uuidString)" defaultsSuites.append(suiteName) let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) let settings = SettingsModel(defaults: defaults) settings.maxHistoryItems = 50 settings.historyRetention = .forever let baseURL = makeTempDirectory() let encryptionService = ClipboardEncryptionService(keyProvider: { nil }) let cacheService = ClipboardCacheService(baseURL: baseURL, encryptionService: encryptionService) let store = ClipboardStore( settings: settings, cacheService: cacheService, baseURL: baseURL, encryptionService: encryptionService ) let controller = SettingsWindowController( settings: settings, store: store, cacheService: cacheService, cloudSyncService: cloudSyncService ) return ControllerFixture(controller: controller, settings: settings, store: store) } private func makeController( cloudSyncService: ClipboardCloudSyncServicing = CloudSyncProbe() ) -> (SettingsWindowController, SettingsModel) { let fixture = makeControllerFixture(cloudSyncService: cloudSyncService) return (fixture.controller, fixture.settings) } private func makeItem(_ payload: String, displayText: String, created: Date) -> ClipboardItem { ClipboardItem( id: UUID(), kind: .text, displayText: displayText, payload: payload, payloadHash: payload, createdAt: created, lastUsedAt: created, useCount: 1, sourceApp: nil, imagePath: nil, thumbnailPath: nil, isPinned: false ) } private func makeTempDirectory() -> URL { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("clipbored-settingswindow-tests", isDirectory: true) .appendingPathComponent(UUID().uuidString, isDirectory: true) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) tempURLs.append(directory) return directory } private func drainMainQueue(iterations: Int = 20) { for _ in 0.. URL { if let archiveURL = statusResponse.archiveURL { return archiveURL } throw ClipboardCloudSyncError.unavailable } func status() -> ClipboardCloudSyncStatus { statusCallCount += 1 return statusResponse } func push(store: ClipboardStore) throws -> ClipboardArchiveSummary { ClipboardArchiveSummary(itemCount: 0, sidecarCount: 0, skippedItemCount: 0, skippedSidecarCount: 0) } func pull(store: ClipboardStore) throws -> ClipboardArchiveSummary { ClipboardArchiveSummary(itemCount: 0, sidecarCount: 0, skippedItemCount: 0, skippedSidecarCount: 0) } }