Files
clipbored/tests/clipboredtests/ClipboardMonitorServiceTests.swift
2026-07-01 15:12:49 -07:00

819 lines
32 KiB
Swift

import XCTest
import Foundation
import AppKit
@testable import ClipBored
final class ClipboardMonitorServiceTests: XCTestCase {
private var tempURLs: [URL] = []
private var suiteNames: [String] = []
override func tearDown() {
tempURLs.forEach { try? FileManager.default.removeItem(at: $0) }
tempURLs.removeAll()
suiteNames.forEach {
UserDefaults(suiteName: $0)?.removePersistentDomain(forName: $0)
}
suiteNames.removeAll()
super.tearDown()
}
func testClampedIntervalEnforcesResponsiveMinimum() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pollProfile = .responsive
let monitor = ClipboardMonitorService(
store: makeStore(settings: settings),
cacheService: ClipboardCacheService(),
settings: settings
)
XCTAssertEqual(monitor.clampedInterval(0.03), AppConfiguration.minResponsiveActiveInterval)
XCTAssertEqual(monitor.clampedInterval(0.05), AppConfiguration.minResponsiveActiveInterval)
XCTAssertEqual(monitor.clampedInterval(0.075), AppConfiguration.minResponsiveActiveInterval)
XCTAssertEqual(monitor.clampedInterval(0.2), 0.2)
}
func testClampedIntervalDoesNotIncreaseBalancedProfileWindow() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pollProfile = .balanced
let monitor = ClipboardMonitorService(
store: makeStore(settings: settings),
cacheService: ClipboardCacheService(),
settings: settings
)
XCTAssertEqual(monitor.clampedInterval(settings.pollProfile.activeInterval), settings.pollProfile.activeInterval)
XCTAssertGreaterThanOrEqual(monitor.clampedInterval(settings.pollProfile.idleInterval), settings.pollProfile.idleInterval)
}
func testPollProfileChangeEmitsDedicatedNotification() {
let settings = SettingsModel(defaults: makeTestDefaults())
var sawPollProfileChange = false
settings.observe { change in
if case .pollProfile = change {
sawPollProfileChange = true
}
}
settings.pollProfile = .responsive
XCTAssertTrue(sawPollProfileChange)
}
func testSetPausedReschedulesWithCurrentPollingProfile() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pollProfile = .battery
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
monitor.setPaused(false)
XCTAssertEqual(monitor.scheduledIntervalForTesting, settings.pollProfile.idleInterval)
settings.pollProfile = .responsive
monitor.setPaused(false)
XCTAssertEqual(monitor.scheduledIntervalForTesting, settings.pollProfile.idleInterval)
monitor.stop()
}
func testPollNowCapturesCopiedTextOnce() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pruneDuplicates = false
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: store,
cacheService: cacheService,
settings: settings
)
let text = "ClipBored monitor smoke \(UUID().uuidString)"
let captured = expectation(description: "copied text captured")
store.observeItems { items in
if items.contains(where: { $0.payload == text }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
XCTAssertEqual(store.items.filter { $0.payload == text }.count, 1)
}
func testPollNowCapturesCodeSnippetAsCode() {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.pruneDuplicates = false
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: store,
cacheService: cacheService,
settings: settings
)
let snippet = "func greet(name: String) -> String {\n return \"Hi \\(name)\"\n}"
let captured = expectation(description: "code snippet captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .code && $0.payload == snippet }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(snippet, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = store.items.first
XCTAssertEqual(item?.kind, .code)
XCTAssertEqual(item?.displayText, "Swift Snippet")
XCTAssertEqual(item?.payload, snippet)
}
func testPollNowIgnoresClipBoredPasteboardWrites() {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let item = ClipboardItem(
id: UUID(),
kind: .text,
displayText: "Internal copy",
payload: "Internal copy \(UUID().uuidString)",
payloadHash: "hash",
createdAt: Date(),
lastUsedAt: Date(),
useCount: 0,
sourceApp: nil,
imagePath: nil,
thumbnailPath: nil
)
XCTAssertEqual(PasteActionService().copy(item), .copied)
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
XCTAssertTrue(store.items.isEmpty)
}
func testPollNowCapturesPDFAsRestorableAttachment() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let pdfData = Data("%PDF-1.4\nclipbored\n%%EOF".utf8)
let captured = expectation(description: "PDF captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .pdf }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(pdfData, forType: .pdf))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first(where: { $0.kind == .pdf }))
XCTAssertTrue(FileManager.default.fileExists(atPath: item.payload))
XCTAssertEqual(cacheService.data(for: item.payload), pdfData)
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
XCTAssertEqual(NSPasteboard.general.data(forType: .pdf), pdfData)
}
func testPollNowCapturesAudioAsRestorableAttachment() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let audioData = Data([1, 2, 3, 4, 8, 16])
let captured = expectation(description: "audio captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .audio }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(audioData, forType: .sound))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first(where: { $0.kind == .audio }))
XCTAssertTrue(FileManager.default.fileExists(atPath: item.payload))
XCTAssertEqual(cacheService.data(for: item.payload), audioData)
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
XCTAssertEqual(NSPasteboard.general.data(forType: .sound), audioData)
}
func testPollNowCapturesColorAsRestorableSwatch() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let color = NSColor(deviceRed: 10 / 255, green: 132 / 255, blue: 255 / 255, alpha: 1)
let captured = expectation(description: "color captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .color && $0.payload == "#0A84FF" }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([color]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first(where: { $0.kind == .color }))
XCTAssertEqual(item.displayText, "#0A84FF")
XCTAssertEqual(item.payload, "#0A84FF")
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
let restored = try XCTUnwrap(NSColor(from: NSPasteboard.general))
XCTAssertEqual(ColorPayload.hexString(from: restored), "#0A84FF")
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "#0A84FF")
}
func testPollNowCapturesFileReference() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let fileURL = try makeTempFile(contents: "file reference")
let captured = expectation(description: "file reference captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .file && $0.payload == fileURL.path }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([fileURL as NSURL]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .file)
XCTAssertEqual(store.items.first?.payload, fileURL.path)
}
func testPollNowCapturesMultipleFileReferencesAsOneRestorableItem() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let firstURL = try makeTempFile(contents: "first file reference")
let secondURL = try makeTempFile(contents: "second file reference")
let payload = FilePayload.payload(from: [firstURL, secondURL])
let captured = expectation(description: "multiple file references captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .file && $0.payload == payload }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([firstURL as NSURL, secondURL as NSURL]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.kind, .file)
XCTAssertEqual(item.displayText, "2 files")
XCTAssertEqual(item.payload, payload)
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
let objects = NSPasteboard.general.readObjects(forClasses: [NSURL.self], options: nil) as? [URL]
XCTAssertEqual(objects?.map(\.standardizedFileURL), [firstURL.standardizedFileURL, secondURL.standardizedFileURL])
}
func testPollNowPrefersFileReferenceOverStringFallback() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let fileURL = try makeTempFile(contents: "file reference with fallback")
let captured = expectation(description: "file reference captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .file && $0.payload == fileURL.path }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([fileURL as NSURL]))
XCTAssertTrue(pasteboard.setString(fileURL.path, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .file)
XCTAssertEqual(store.items.first?.payload, fileURL.path)
}
func testPollNowCapturesBareURLAsLink() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let url = "https://example.com/releases"
let captured = expectation(description: "URL captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .url && $0.payload == url }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(url, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .url)
XCTAssertEqual(store.items.first?.payload, url)
}
func testPollNowUsesPasteboardURLNameAsLinkTitle() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let url = "https://example.com/releases"
let title = "Release notes"
let captured = expectation(description: "URL with title captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .url && $0.payload == url && $0.displayText == title }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(url, forType: .URL))
XCTAssertTrue(pasteboard.setString(title, forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")))
XCTAssertTrue(pasteboard.setString(url, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .url)
XCTAssertEqual(store.items.first?.displayText, title)
XCTAssertEqual(store.items.first?.payload, url)
}
func testPollNowUsesHTMLAnchorTextAsLinkTitle() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let url = "https://example.com/releases"
let title = "Read the release notes"
let html = "<a href=\"\(url)\">\(title)</a>"
let captured = expectation(description: "URL with HTML title captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .url && $0.payload == url && $0.displayText == title }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(url, forType: .URL))
XCTAssertTrue(pasteboard.setString(html, forType: .html))
XCTAssertTrue(pasteboard.setString(url, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .url)
XCTAssertEqual(store.items.first?.displayText, title)
XCTAssertEqual(store.items.first?.payload, url)
}
func testPollNowCapturesURLWithLocalImagePreviewAsLink() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let url = "https://example.com/lookbook"
let title = "Lookbook"
let preview = makeImage(color: .systemTeal)
let captured = expectation(description: "URL with local preview captured")
store.observeItems { items in
if let item = items.first, item.kind == .url, item.payload == url, item.thumbnailPath != nil {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(url, forType: .URL))
XCTAssertTrue(pasteboard.setString(title, forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")))
XCTAssertTrue(pasteboard.setData(try XCTUnwrap(preview.tiffRepresentation), forType: .tiff))
XCTAssertTrue(pasteboard.setString(url, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
cacheService.flushForTesting()
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.kind, .url)
XCTAssertEqual(item.displayText, title)
XCTAssertNotNil(item.imagePath)
XCTAssertNotNil(item.thumbnailPath)
XCTAssertNotNil(cacheService.previewThumbnail(for: item))
}
func testPollNowCapturesImageWithRecognizedTextWhenSearchIsEnabled() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.includeImageTextInSearch = true
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(
store: store,
cacheService: cacheService,
settings: settings,
imageTextExtractor: { _ in " Receipt total $42\nOrder 1001 " }
)
let image = makeImage(color: .systemOrange)
let captured = expectation(description: "image with recognized text captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .image && $0.ocrText == "Receipt total $42 Order 1001" }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([image]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.kind, .image)
XCTAssertEqual(item.ocrText, "Receipt total $42 Order 1001")
XCTAssertTrue(FileManager.default.fileExists(atPath: item.payload))
}
func testPollNowSkipsImageTextExtractionWhenSearchIsDisabled() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.includeImageTextInSearch = false
let (store, cacheService) = makeStoreAndCache(settings: settings)
var extractionCount = 0
let monitor = ClipboardMonitorService(
store: store,
cacheService: cacheService,
settings: settings,
imageTextExtractor: { _ in
extractionCount += 1
return "Should not be captured"
}
)
let image = makeImage(color: .systemPurple)
let captured = expectation(description: "image captured without recognized text")
store.observeItems { items in
if items.contains(where: { $0.kind == .image }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([image]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.kind, .image)
XCTAssertNil(item.ocrText)
XCTAssertEqual(extractionCount, 0)
}
func testIgnoredFileSettingDoesNotBlockWebURLObjects() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.file.rawValue]
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let url = "https://example.com/releases"
let captured = expectation(description: "web URL captured when files are ignored")
store.observeItems { items in
if items.contains(where: { $0.kind == .url && $0.payload == url }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([NSURL(string: url)!]))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .url)
XCTAssertEqual(store.items.first?.payload, url)
}
func testPollNowKeepsTextContainingURLAsText() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let text = "Review https://example.com/releases before shipping"
let captured = expectation(description: "text with URL captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .text && $0.payload == text }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .text)
XCTAssertEqual(store.items.first?.payload, text)
}
func testPollNowPrefersRichTextOverPlainStringFallback() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let text = "Rich clipboard text"
let attributed = NSAttributedString(
string: text,
attributes: [.font: NSFont.boldSystemFont(ofSize: 13)]
)
let rtfData = try XCTUnwrap(
attributed.rtf(from: NSRange(location: 0, length: attributed.length), documentAttributes: [:])
)
let captured = expectation(description: "rich text captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .richText && $0.displayText == text }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(rtfData, forType: .rtf))
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
XCTAssertEqual(store.items.first?.kind, .richText)
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.displayText, text)
XCTAssertTrue(FileManager.default.fileExists(atPath: item.payload))
XCTAssertEqual(cacheService.data(for: item.payload), rtfData)
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
XCTAssertEqual(NSPasteboard.general.data(forType: .rtf), rtfData)
XCTAssertEqual(NSPasteboard.general.string(forType: .string), text)
}
func testPollNowCapturesHTMLClipboardDataAsRestorableRichText() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
let (store, cacheService) = makeStoreAndCache(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let text = "Styled HTML clipboard text"
let html = """
<span style="font-weight: 700; color: #0a84ff;">Styled HTML</span> clipboard text
"""
let htmlData = Data(html.utf8)
let captured = expectation(description: "HTML rich text captured")
store.observeItems { items in
if items.contains(where: { $0.kind == .richText && $0.displayText == text }) {
captured.fulfill()
}
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(htmlData, forType: .html))
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
wait(for: [captured], timeout: 1.0)
let item = try XCTUnwrap(store.items.first)
XCTAssertEqual(item.kind, .richText)
XCTAssertEqual(item.displayText, text)
XCTAssertTrue(FileManager.default.fileExists(atPath: item.payload))
let cachedRTF = try XCTUnwrap(cacheService.data(for: item.payload))
XCTAssertNotNil(NSAttributedString(rtf: cachedRTF, documentAttributes: nil))
XCTAssertEqual(PasteActionService(cacheService: cacheService).copy(item), .copied)
XCTAssertNotNil(NSPasteboard.general.data(forType: .rtf))
XCTAssertEqual(NSPasteboard.general.string(forType: .string), text)
}
func testIgnoredImageKindDoesNotWriteCacheFiles() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.image.rawValue]
let (store, cacheService, baseURL) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let image = makeImage(color: .systemOrange)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([image]))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
cacheService.flushForTesting()
XCTAssertTrue(store.items.isEmpty)
XCTAssertTrue(try imageCacheFileURLs(in: baseURL).isEmpty)
}
func testIgnoredColorKindDoesNotCaptureSwatch() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.color.rawValue]
let (store, cacheService, _) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let color = NSColor(deviceRed: 10 / 255, green: 132 / 255, blue: 255 / 255, alpha: 1)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.writeObjects([color]))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
XCTAssertTrue(store.items.isEmpty)
XCTAssertEqual(settings.captureStatusMessage, "Skipped: Color items are ignored in capture settings.")
}
func testIgnoredCodeKindDoesNotCaptureSnippet() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.code.rawValue]
let (store, cacheService, _) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let snippet = "const title = \"ClipBored\";\nreturn title.toUpperCase();"
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setString(snippet, forType: .string))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
XCTAssertTrue(store.items.isEmpty)
XCTAssertEqual(settings.captureStatusMessage, "Skipped: Code items are ignored in capture settings.")
}
func testIgnoredPDFKindDoesNotWriteAttachmentFiles() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.pdf.rawValue]
let (store, cacheService, baseURL) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(Data("%PDF-1.4\n%%EOF".utf8), forType: .pdf))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
cacheService.flushForTesting()
XCTAssertTrue(store.items.isEmpty)
XCTAssertTrue(try attachmentFileURLs(in: baseURL).isEmpty)
XCTAssertEqual(settings.captureStatusMessage, "Skipped: PDF items are ignored in capture settings.")
}
func testIgnoredAudioKindDoesNotWriteAttachmentFiles() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.audio.rawValue]
let (store, cacheService, baseURL) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(Data([3, 1, 4, 1, 5]), forType: .sound))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
cacheService.flushForTesting()
XCTAssertTrue(store.items.isEmpty)
XCTAssertTrue(try attachmentFileURLs(in: baseURL).isEmpty)
XCTAssertEqual(settings.captureStatusMessage, "Skipped: Audio items are ignored in capture settings.")
}
func testIgnoredRichTextKindDoesNotWriteHTMLAttachmentFiles() throws {
let settings = SettingsModel(defaults: makeTestDefaults())
settings.ignoredItemKindsRaw = [ClipboardItemKind.richText.rawValue]
let (store, cacheService, baseURL) = makeStoreCacheAndBaseURL(settings: settings)
let monitor = ClipboardMonitorService(store: store, cacheService: cacheService, settings: settings)
let text = "Ignored HTML clipboard text"
let html = "<strong>Ignored HTML</strong> clipboard text"
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
XCTAssertTrue(pasteboard.setData(Data(html.utf8), forType: .html))
XCTAssertTrue(pasteboard.setString(text, forType: .string))
monitor.pollNowAndWait()
RunLoop.main.run(until: Date().addingTimeInterval(0.05))
cacheService.flushForTesting()
XCTAssertTrue(store.items.isEmpty)
XCTAssertTrue(try attachmentFileURLs(in: baseURL).isEmpty)
XCTAssertEqual(settings.captureStatusMessage, "Skipped: Rich Text items are ignored in capture settings.")
}
private func makeTestDefaults() -> UserDefaults {
let suiteName = "com.clipbored.testmonitor.\(UUID().uuidString)"
suiteNames.append(suiteName)
return UserDefaults(suiteName: suiteName)!
}
private func makeStore(settings: SettingsModel) -> ClipboardStore {
makeStoreAndCache(settings: settings).store
}
private func makeStoreAndCache(settings: SettingsModel) -> (store: ClipboardStore, cacheService: ClipboardCacheService) {
let result = makeStoreCacheAndBaseURL(settings: settings)
return (result.store, result.cacheService)
}
private func makeStoreCacheAndBaseURL(settings: SettingsModel) -> (store: ClipboardStore, cacheService: ClipboardCacheService, baseURL: URL) {
let baseURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clipboredtests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try? FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
tempURLs.append(baseURL)
let cacheService = ClipboardCacheService(
baseURL: baseURL,
encryptionService: ClipboardEncryptionService(keyProvider: { nil })
)
return (
ClipboardStore(
settings: settings,
cacheService: cacheService,
baseURL: baseURL,
encryptionService: ClipboardEncryptionService(keyProvider: { nil })
),
cacheService,
baseURL
)
}
private func makeTempFile(contents: String) throws -> URL {
let baseURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clipboredtests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
tempURLs.append(baseURL)
let fileURL = baseURL.appendingPathComponent("payload.txt")
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
return fileURL
}
private func imageCacheFileURLs(in baseURL: URL) throws -> [URL] {
let imageDirectory = baseURL.appendingPathComponent("images", isDirectory: true)
return try FileManager.default.contentsOfDirectory(at: imageDirectory, includingPropertiesForKeys: nil)
}
private func attachmentFileURLs(in baseURL: URL) throws -> [URL] {
let attachmentDirectory = baseURL.appendingPathComponent("attachments", isDirectory: true)
return try FileManager.default.contentsOfDirectory(at: attachmentDirectory, includingPropertiesForKeys: nil)
}
private func makeImage(color: NSColor) -> NSImage {
let size = NSSize(width: 24, height: 24)
let image = NSImage(size: size)
image.lockFocus()
color.setFill()
NSRect(origin: .zero, size: size).fill()
image.unlockFocus()
return image
}
}