import XCTest import AppKit import PDFKit @testable import IHatePDFsCore final class AnnotationFactoryTests: XCTestCase { func testHighlightSelectionRoundTripsThroughPDFSave() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertions = AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "Use this passage in lecture.", author: "Professor" ) XCTAssertEqual(insertions.count, 1) for insertion in insertions { insertion.page.addAnnotation(insertion.annotation) if let popup = insertion.popup { insertion.page.addAnnotation(popup) } } let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("pdf") defer { try? FileManager.default.removeItem(at: outputURL) } XCTAssertTrue(document.write(to: outputURL)) let reopened = try XCTUnwrap(PDFDocument(url: outputURL)) let reopenedPage = try XCTUnwrap(reopened.page(at: 0)) XCTAssertEqual(reopenedPage.string, "This is selectable academic text.") XCTAssertTrue( reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .highlight) && $0.contents == "Use this passage in lecture." } ) } func testHighlightUsesHigherContrastDefaultColor() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "", author: "Professor" ).first ) let annotationColor = try rgbaComponents(insertion.annotation.color) let defaultColor = try rgbaComponents(AcademicAnnotationPalette.highlight) XCTAssertEqual(annotationColor.red, defaultColor.red, accuracy: 0.001) XCTAssertEqual(annotationColor.green, defaultColor.green, accuracy: 0.001) XCTAssertEqual(annotationColor.blue, defaultColor.blue, accuracy: 0.001) XCTAssertEqual(annotationColor.alpha, defaultColor.alpha, accuracy: 0.001) XCTAssertGreaterThanOrEqual(annotationColor.alpha, 0.5) } func testHighlightCreatedWithoutCommentHasNoPopupOrCommentText() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "", author: "Professor" ).first ) XCTAssertNil(insertion.popup) XCTAssertEqual(AnnotationKeys.commentText(for: insertion.annotation), "") XCTAssertNil(insertion.annotation.contents) XCTAssertEqual(AcademicAnnotationKind(annotation: insertion.annotation), .highlight) } func testHighlightUsesConfiguredColor() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let configuredColor = NSColor( calibratedRed: 0.18, green: 0.58, blue: 0.95, alpha: 0.52 ) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "", author: "Professor", highlightColor: configuredColor ).first ) try XCTAssertColor(insertion.annotation.color, equals: configuredColor) } func testSelectionBoundCommentUsesConfiguredColor() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10))) let configuredColor = NSColor( calibratedRed: 0.88, green: 0.18, blue: 0.26, alpha: 0.34 ) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "", author: "Professor", commentColor: configuredColor ).first ) try XCTAssertColor(insertion.annotation.color, equals: configuredColor) XCTAssertEqual( insertion.annotation.value(forAnnotationKey: AnnotationKeys.appKind) as? String, AnnotationKeys.appKindComment ) } func testSelectionBoundCommentRoundTripsAsCommentKind() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "Explain this phrase.", author: "Professor" ).first ) page.addAnnotation(insertion.annotation) if let popup = insertion.popup { page.addAnnotation(popup) } let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() let reopenedComment = try XCTUnwrap(reopenedPage.annotations.first { AnnotationKeys.annotation($0, hasSubtype: .highlight) && $0.contents == "Explain this phrase." }) XCTAssertEqual(AcademicAnnotationKind(annotation: reopenedComment), .comment) XCTAssertEqual( reopenedComment.value(forAnnotationKey: AnnotationKeys.appKind) as? String, AnnotationKeys.appKindComment ) } func testSelectionBoundCommentCreatedEmptyGetsPopupWhenTextIsSaved() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "", author: "Professor" ).first ) XCTAssertNil(insertion.popup) page.addAnnotation(insertion.annotation) let popup = try XCTUnwrap(AnnotationFactory.updateComment( for: insertion.annotation, on: page, text: "Explain this phrase.", author: "Professor" )) page.addAnnotation(popup) let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() let reopenedComment = try XCTUnwrap(reopenedPage.annotations.first { AnnotationKeys.annotation($0, hasSubtype: .highlight) && $0.contents == "Explain this phrase." }) XCTAssertEqual(AcademicAnnotationKind(annotation: reopenedComment), .comment) XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) && $0.contents == "Explain this phrase." }) } func testPopupMarkerIsPlacedInRightPageMargin() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "Margin marker.", author: "Professor" ).first ) let popup = try XCTUnwrap(insertion.popup) let pageBounds = page.bounds(for: .cropBox) XCTAssertEqual(popup.bounds.minX, pageBounds.maxX - 28, accuracy: 0.01) XCTAssertGreaterThanOrEqual(popup.bounds.minY, pageBounds.minY + 12) XCTAssertLessThanOrEqual(popup.bounds.minY, pageBounds.maxY - 40) popup.bounds = insertion.annotation.bounds.offsetBy(dx: 10, dy: 0) XCTAssertTrue(AnnotationFactory.normalizePopupPlacement(for: insertion.annotation, on: page)) XCTAssertEqual(popup.bounds.minX, pageBounds.maxX - 28, accuracy: 0.01) } func testDetachingPopupForViewerKeepsSidebarCommentText() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "", author: "Professor" ).first ) page.addAnnotation(insertion.annotation) let popup = try XCTUnwrap(AnnotationFactory.updateComment( for: insertion.annotation, on: page, text: "Visible comment text.", author: "Professor" )) page.addAnnotation(popup) XCTAssertTrue(AnnotationFactory.detachPopupForViewer(from: insertion.annotation, on: page)) XCTAssertNil(insertion.annotation.popup) XCTAssertNil(insertion.annotation.contents) XCTAssertEqual(AnnotationKeys.commentText(for: insertion.annotation), "Visible comment text.") XCTAssertFalse(page.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) }) let snapshot = try XCTUnwrap(AnnotationReader.snapshots(in: document).first { $0.annotation === insertion.annotation }) XCTAssertEqual(snapshot.contents, "Visible comment text.") } func testRestoreCommentTextForExportWritesStandardPDFContents() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .comment, comment: "Exported comment text.", author: "Professor" ).first ) page.addAnnotation(insertion.annotation) if let popup = insertion.popup { page.addAnnotation(popup) } AnnotationFactory.detachPopupForViewer(from: insertion.annotation, on: page) XCTAssertNil(insertion.annotation.contents) XCTAssertTrue(AnnotationFactory.restoreCommentTextForExport(insertion.annotation)) let popup = try XCTUnwrap(AnnotationFactory.makePopupIfNeeded( for: insertion.annotation, on: page, open: false )) if popup.page == nil { page.addAnnotation(popup) } let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .highlight) && $0.contents == "Exported comment text." }) } func testPreviewCompatibleExportKeepsMarkupCommentWithoutPopupAnnotation() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let insertion = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "Preview should show this comment.", author: "Professor" ).first ) page.addAnnotation(insertion.annotation) if let popup = insertion.popup { page.addAnnotation(popup) } XCTAssertTrue(AnnotationFactory.prepareForPreviewCompatibleExport(insertion.annotation, on: page)) XCTAssertNil(insertion.annotation.popup) XCTAssertEqual(insertion.annotation.contents, "Preview should show this comment.") XCTAssertFalse(page.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) }) let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() let highlights = reopenedPage.annotations.filter { AnnotationKeys.annotation($0, hasSubtype: .highlight) } XCTAssertEqual(highlights.count, 1) XCTAssertEqual(highlights.first?.contents, "Preview should show this comment.") XCTAssertFalse(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) }) } func testPreviewCompatibleExportRecoversPopupOnlyCommentText() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29))) let bounds = selection.bounds(for: page) let annotation = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil) annotation.markupType = .highlight annotation.color = AcademicAnnotationPalette.highlight annotation.userName = "Professor" annotation.quadrilateralPoints = [ NSValue(point: CGPoint(x: 0, y: bounds.height)), NSValue(point: CGPoint(x: bounds.width, y: bounds.height)), NSValue(point: .zero), NSValue(point: CGPoint(x: bounds.width, y: 0)) ] let popup = PDFAnnotation( bounds: CGRect(x: 360, y: 620, width: 220, height: 90), forType: .popup, withProperties: nil ) popup.contents = "Popup-only comment from another reader." annotation.popup = popup page.addAnnotation(annotation) page.addAnnotation(popup) XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Popup-only comment from another reader.") XCTAssertTrue(AnnotationFactory.prepareForPreviewCompatibleExport(annotation, on: page)) XCTAssertNil(annotation.popup) XCTAssertEqual(annotation.contents, "Popup-only comment from another reader.") XCTAssertFalse(page.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) }) } func testEmptyAppCommentTextFallsBackToStandardContents() throws { let annotation = PDFAnnotation( bounds: CGRect(x: 72, y: 620, width: 260, height: 24), forType: .highlight, withProperties: nil ) AnnotationKeys.setCommentText("", for: annotation) annotation.contents = "Comment added by another PDF reader." XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Comment added by another PDF reader.") } func testEmptyAppCommentTextFallsBackToPopupContents() throws { let annotation = PDFAnnotation( bounds: CGRect(x: 72, y: 620, width: 260, height: 24), forType: .highlight, withProperties: nil ) let popup = PDFAnnotation( bounds: CGRect(x: 360, y: 620, width: 220, height: 90), forType: .popup, withProperties: nil ) AnnotationKeys.setCommentText("", for: annotation) popup.contents = "Popup comment added by another PDF reader." annotation.popup = popup XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Popup comment added by another PDF reader.") } func testAddingAnnotationPreservesPriorAnnotation() throws { let document = try makeSelectableTextDocument() let page = try XCTUnwrap(document.page(at: 0)) let prior = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 420, y: 700), comment: "Existing note from another reader.", author: "Colleague" ) page.addAnnotation(prior.annotation) if let popup = prior.popup { page.addAnnotation(popup) } let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10))) let highlight = try XCTUnwrap( AnnotationFactory.markupInsertions( from: selection, style: .highlight, comment: "New professor comment.", author: "Professor" ).first ) page.addAnnotation(highlight.annotation) if let popup = highlight.popup { page.addAnnotation(popup) } let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() XCTAssertEqual(reopenedPage.string, "This is selectable academic text.") XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .text) && $0.contents == "Existing note from another reader." }) XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .highlight) && $0.contents == "New professor comment." }) } func testScannedImagePDFCanReceiveStandardTextAnnotation() throws { let document = try makeImageOnlyDocument() let page = try XCTUnwrap(document.page(at: 0)) let insertion = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 300, y: 500), comment: "Comment on scanned reading.", author: "Professor" ) page.addAnnotation(insertion.annotation) if let popup = insertion.popup { page.addAnnotation(popup) } let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap() XCTAssertNil(reopenedPage.string) XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .text) && $0.contents == "Comment on scanned reading." }) } func testFiveHundredPagePDFCanBeSavedWithAnnotation() throws { let document = PDFDocument() for index in 0..<501 { document.insert(PDFPage(), at: index) } XCTAssertEqual(document.pageCount, 501) let page = try XCTUnwrap(document.page(at: 500)) let insertion = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 120, y: 120), comment: "End of long reading.", author: "Professor" ) page.addAnnotation(insertion.annotation) if let popup = insertion.popup { page.addAnnotation(popup) } let reopened = try saveAndReopen(document) XCTAssertEqual(reopened.pageCount, 501) let reopenedPage = try XCTUnwrap(reopened.page(at: 500)) XCTAssertTrue(reopenedPage.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .text) && $0.contents == "End of long reading." }) } func testTextAnnotationUsesStandardKeys() throws { let page = PDFPage() let insertion = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 100, y: 100), comment: "Discuss this claim in class.", author: "Professor" ) XCTAssertTrue(AnnotationKeys.annotation(insertion.annotation, hasSubtype: .text)) XCTAssertEqual(insertion.annotation.contents, "Discuss this claim in class.") XCTAssertEqual(insertion.annotation.userName, "Professor") XCTAssertNotNil(insertion.annotation.value(forAnnotationKey: .name)) XCTAssertNotNil(insertion.annotation.value(forAnnotationKey: AnnotationKeys.creationDate)) XCTAssertEqual(insertion.annotation.value(forAnnotationKey: AnnotationKeys.state) as? String, "Unmarked") XCTAssertNotNil(insertion.popup) XCTAssertTrue(insertion.popup.map { AnnotationKeys.annotation($0, hasSubtype: .popup) } ?? false) } func testReplyStoresHiddenTextAnnotationWithBestEffortParentID() throws { let page = PDFPage() let parent = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 100, y: 100), comment: "Parent", author: "Professor" ).annotation let reply = AnnotationFactory.replyInsertion( to: parent, on: page, comment: "Reply", author: "Reader", parentID: "parent-id" ) XCTAssertTrue(AnnotationKeys.annotation(reply.annotation, hasSubtype: .text)) XCTAssertEqual(reply.annotation.value(forAnnotationKey: AnnotationKeys.inReplyTo) as? String, "parent-id") XCTAssertEqual(reply.annotation.value(forAnnotationKey: AnnotationKeys.replyType) as? String, "R") XCTAssertFalse(reply.annotation.shouldDisplay) XCTAssertFalse(reply.annotation.shouldPrint) page.addAnnotation(reply.annotation) AnnotationFactory.hideReplyMarker(reply.annotation, on: page) XCTAssertFalse(reply.annotation.shouldDisplay) XCTAssertFalse(reply.annotation.shouldPrint) XCTAssertEqual(AnnotationKeys.commentText(for: reply.annotation), "Reply") XCTAssertGreaterThan(reply.annotation.bounds.minX, page.bounds(for: .cropBox).maxX) XCTAssertGreaterThan(reply.annotation.bounds.minY, page.bounds(for: .cropBox).maxY) XCTAssertNil(reply.popup) } func testStringReplyParentIDResolvesToParentStableID() throws { let document = PDFDocument() let page = PDFPage() document.insert(page, at: 0) let parent = AnnotationFactory.noteInsertion( on: page, near: CGPoint(x: 100, y: 100), comment: "Parent", author: "Professor" ).annotation page.addAnnotation(parent) let parentID = try XCTUnwrap(parent.value(forAnnotationKey: .name) as? String) let reply = AnnotationFactory.replyInsertion( to: parent, on: page, comment: "Reply", author: "Reader", parentID: parentID ).annotation page.addAnnotation(reply) AnnotationFactory.hideReplyMarker(reply, on: page) let snapshots = AnnotationReader.snapshots(in: document) let parentSnapshot = try XCTUnwrap(snapshots.first { $0.contents == "Parent" }) let replySnapshot = try XCTUnwrap(snapshots.first { $0.contents == "Reply" }) XCTAssertEqual(replySnapshot.kind, .reply) XCTAssertEqual(replySnapshot.parentID, parentSnapshot.id) } func testUnresolvedStringReplyParentIDStaysVisibleAsTopLevelReply() throws { let document = PDFDocument() let page = PDFPage() document.insert(page, at: 0) let orphanedReply = PDFAnnotation( bounds: CGRect(x: 100, y: 100, width: 24, height: 24), forType: .text, withProperties: nil ) AnnotationFactory.standardize( orphanedReply, comment: "Reply from an external reader.", author: "Reader", date: Date() ) _ = orphanedReply.setValue("missing-parent", forAnnotationKey: AnnotationKeys.inReplyTo) _ = orphanedReply.setValue("R", forAnnotationKey: AnnotationKeys.replyType) page.addAnnotation(orphanedReply) let snapshot = try XCTUnwrap(AnnotationReader.snapshots(in: document).first) XCTAssertEqual(snapshot.kind, .reply) XCTAssertNil(snapshot.parentID) XCTAssertFalse(snapshot.isReply) XCTAssertEqual(snapshot.contents, "Reply from an external reader.") } func testPageScopedSnapshotsOnlyReadRequestedPages() throws { let document = PDFDocument() let firstPage = PDFPage() let secondPage = PDFPage() let thirdPage = PDFPage() document.insert(firstPage, at: 0) document.insert(secondPage, at: 1) document.insert(thirdPage, at: 2) let firstAnnotation = AnnotationFactory.noteInsertion( on: firstPage, near: CGPoint(x: 100, y: 100), comment: "First page note", author: "Professor" ).annotation firstPage.addAnnotation(firstAnnotation) let thirdAnnotation = AnnotationFactory.noteInsertion( on: thirdPage, near: CGPoint(x: 200, y: 200), comment: "Third page note", author: "Professor" ).annotation thirdPage.addAnnotation(thirdAnnotation) let scopedSnapshots = AnnotationReader.snapshots(in: document, pages: [thirdPage, thirdPage]) XCTAssertEqual(scopedSnapshots.count, 1) XCTAssertEqual(scopedSnapshots.first?.contents, "Third page note") XCTAssertEqual(scopedSnapshots.first?.pageIndex, 2) XCTAssertFalse(scopedSnapshots.contains { $0.annotation === firstAnnotation }) } func testFreeTextCreatesStandardFreeTextAnnotation() throws { let page = PDFPage() let insertion = AnnotationFactory.freeTextInsertion( on: page, near: CGPoint(x: 200, y: 200), text: "Important definition", author: "Professor" ) XCTAssertTrue(AnnotationKeys.annotation(insertion.annotation, hasSubtype: .freeText)) XCTAssertEqual(insertion.annotation.contents, "Important definition") XCTAssertNil(insertion.popup) } private func makeSelectableTextDocument() throws -> PDFDocument { let data = NSMutableData() var mediaBox = CGRect(x: 0, y: 0, width: 612, height: 792) let consumer = try XCTUnwrap(CGDataConsumer(data: data)) let context = try XCTUnwrap(CGContext(consumer: consumer, mediaBox: &mediaBox, nil)) context.beginPDFPage(nil) NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) let text = NSAttributedString( string: "This is selectable academic text.", attributes: [.font: NSFont.systemFont(ofSize: 18)] ) text.draw(at: CGPoint(x: 72, y: 700)) NSGraphicsContext.restoreGraphicsState() context.endPDFPage() context.closePDF() return try XCTUnwrap(PDFDocument(data: data as Data)) } private func makeImageOnlyDocument() throws -> PDFDocument { let image = NSImage(size: CGSize(width: 612, height: 792)) image.lockFocus() NSColor.white.setFill() NSRect(x: 0, y: 0, width: 612, height: 792).fill() NSColor.darkGray.setStroke() let path = NSBezierPath(rect: NSRect(x: 72, y: 580, width: 468, height: 80)) path.lineWidth = 2 path.stroke() image.unlockFocus() let document = PDFDocument() document.insert(try XCTUnwrap(PDFPage(image: image)), at: 0) return document } private func saveAndReopen(_ document: PDFDocument) throws -> PDFDocument { let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("pdf") XCTAssertTrue(document.write(to: outputURL)) let reopened = try XCTUnwrap(PDFDocument(url: outputURL)) try? FileManager.default.removeItem(at: outputURL) return reopened } private func XCTAssertColor( _ actual: NSColor, equals expected: NSColor, file: StaticString = #filePath, line: UInt = #line ) throws { let actualComponents = try rgbaComponents(actual, file: file, line: line) let expectedComponents = try rgbaComponents(expected, file: file, line: line) XCTAssertEqual(actualComponents.red, expectedComponents.red, accuracy: 0.001, file: file, line: line) XCTAssertEqual(actualComponents.green, expectedComponents.green, accuracy: 0.001, file: file, line: line) XCTAssertEqual(actualComponents.blue, expectedComponents.blue, accuracy: 0.001, file: file, line: line) XCTAssertEqual(actualComponents.alpha, expectedComponents.alpha, accuracy: 0.001, file: file, line: line) } private func rgbaComponents( _ color: NSColor, file: StaticString = #filePath, line: UInt = #line ) throws -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { let rgb = try XCTUnwrap(color.usingColorSpace(.deviceRGB), file: file, line: line) var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 rgb.getRed(&red, green: &green, blue: &blue, alpha: &alpha) return (red, green, blue, alpha) } } private extension Optional { func unwrap( file: StaticString = #filePath, line: UInt = #line ) throws -> Wrapped { try XCTUnwrap(self, file: file, line: line) } }