Clean up repository structure and release docs
This commit is contained in:
74
tests/core/AnnotationColorPreferenceTests.swift
Normal file
74
tests/core/AnnotationColorPreferenceTests.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import XCTest
|
||||
import AppKit
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class AnnotationColorPreferenceTests: XCTestCase {
|
||||
func testColorPreferenceRoundTripsRGBAStorage() throws {
|
||||
let color = NSColor(deviceRed: 0.25, green: 0.5, blue: 0.75, alpha: 0.4)
|
||||
let storage = AnnotationColorPreference.storageString(for: color)
|
||||
XCTAssertEqual(storage, "#4080BF66")
|
||||
|
||||
let decoded = AnnotationColorPreference.color(
|
||||
from: storage,
|
||||
fallback: AcademicAnnotationPalette.highlight
|
||||
)
|
||||
let components = try rgbaComponents(decoded)
|
||||
|
||||
XCTAssertEqual(components.red, 0x40 / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.green, 0x80 / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.blue, 0xBF / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.alpha, 0x66 / 255, accuracy: 0.001)
|
||||
}
|
||||
|
||||
func testColorPreferenceUsesFallbackForInvalidStorage() throws {
|
||||
let decoded = AnnotationColorPreference.color(
|
||||
from: "not-a-color",
|
||||
fallback: AcademicAnnotationPalette.comment
|
||||
)
|
||||
|
||||
try XCTAssertColor(decoded, equals: AcademicAnnotationPalette.comment)
|
||||
}
|
||||
|
||||
func testColorPreferenceAppliesMinimumAlphaWithoutChangingRGB() throws {
|
||||
let decoded = AnnotationColorPreference.color(
|
||||
from: "#33669905",
|
||||
fallback: AcademicAnnotationPalette.highlight,
|
||||
minimumAlpha: 0.3
|
||||
)
|
||||
let components = try rgbaComponents(decoded)
|
||||
|
||||
XCTAssertEqual(components.red, 0x33 / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.green, 0x66 / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.blue, 0x99 / 255, accuracy: 0.001)
|
||||
XCTAssertEqual(components.alpha, 0.3, accuracy: 0.001)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
733
tests/core/AnnotationFactoryTests.swift
Normal file
733
tests/core/AnnotationFactoryTests.swift
Normal file
@@ -0,0 +1,733 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
38
tests/core/AnnotationHitTestingTests.swift
Normal file
38
tests/core/AnnotationHitTestingTests.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import XCTest
|
||||
import PDFKit
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class AnnotationHitTestingTests: XCTestCase {
|
||||
func testTextMarkupHitTestingUsesQuadPointsInsteadOfUnionBounds() {
|
||||
let annotation = PDFAnnotation(
|
||||
bounds: CGRect(x: 10, y: 20, width: 100, height: 60),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
annotation.quadrilateralPoints = [
|
||||
NSValue(point: CGPoint(x: 0, y: 55)),
|
||||
NSValue(point: CGPoint(x: 100, y: 55)),
|
||||
NSValue(point: CGPoint(x: 0, y: 45)),
|
||||
NSValue(point: CGPoint(x: 100, y: 45)),
|
||||
NSValue(point: CGPoint(x: 0, y: 15)),
|
||||
NSValue(point: CGPoint(x: 100, y: 15)),
|
||||
NSValue(point: CGPoint(x: 0, y: 5)),
|
||||
NSValue(point: CGPoint(x: 100, y: 5))
|
||||
]
|
||||
|
||||
XCTAssertTrue(AnnotationHitTesting.containsTextMarkupPoint(CGPoint(x: 50, y: 70), in: annotation))
|
||||
XCTAssertTrue(AnnotationHitTesting.containsTextMarkupPoint(CGPoint(x: 50, y: 30), in: annotation))
|
||||
XCTAssertFalse(AnnotationHitTesting.containsTextMarkupPoint(CGPoint(x: 50, y: 50), in: annotation))
|
||||
}
|
||||
|
||||
func testTextMarkupHitTestingFallsBackToBoundsWithoutQuadPoints() {
|
||||
let annotation = PDFAnnotation(
|
||||
bounds: CGRect(x: 10, y: 20, width: 100, height: 20),
|
||||
forType: .underline,
|
||||
withProperties: nil
|
||||
)
|
||||
|
||||
XCTAssertTrue(AnnotationHitTesting.containsTextMarkupPoint(CGPoint(x: 50, y: 30), in: annotation))
|
||||
XCTAssertFalse(AnnotationHitTesting.containsTextMarkupPoint(CGPoint(x: 50, y: 60), in: annotation))
|
||||
}
|
||||
}
|
||||
52
tests/core/PDFDocumentBookmarksTests.swift
Normal file
52
tests/core/PDFDocumentBookmarksTests.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import XCTest
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class PDFDocumentBookmarksTests: XCTestCase {
|
||||
func testUpsertReplacesExistingBookmark() {
|
||||
let first = PDFDocumentBookmark(id: "first", pageIndex: 4, pageLabel: "5", title: "Old")
|
||||
let second = PDFDocumentBookmark(id: "second", pageIndex: 1, pageLabel: "2", title: "Second")
|
||||
let replacement = PDFDocumentBookmark(id: "replacement", pageIndex: 4, pageLabel: "5", title: "New")
|
||||
|
||||
let result = PDFDocumentBookmarks.upsert(replacement, in: [first, second])
|
||||
|
||||
XCTAssertEqual(result.map(\.id), ["replacement"])
|
||||
XCTAssertEqual(result.first?.title, "New")
|
||||
}
|
||||
|
||||
func testRemovingBookmarkCollapsesDirtyMultipleBookmarkData() {
|
||||
let first = PDFDocumentBookmark(id: "first", pageIndex: 0, pageLabel: "1", title: "First")
|
||||
let second = PDFDocumentBookmark(id: "second", pageIndex: 1, pageLabel: "2", title: "Second")
|
||||
|
||||
let result = PDFDocumentBookmarks.removing(id: "first", from: [first, second])
|
||||
|
||||
XCTAssertEqual(result.map(\.id), ["second"])
|
||||
}
|
||||
|
||||
func testClampedDropsInvalidBookmarksAndKeepsOneBookmark() {
|
||||
let older = PDFDocumentBookmark(
|
||||
id: "older",
|
||||
pageIndex: 0,
|
||||
pageLabel: "1",
|
||||
title: "Older",
|
||||
createdAt: Date(timeIntervalSince1970: 100)
|
||||
)
|
||||
let newer = PDFDocumentBookmark(
|
||||
id: "newer",
|
||||
pageIndex: 1,
|
||||
pageLabel: "2",
|
||||
title: "Newer",
|
||||
createdAt: Date(timeIntervalSince1970: 200)
|
||||
)
|
||||
let invalid = PDFDocumentBookmark(
|
||||
id: "invalid",
|
||||
pageIndex: 4,
|
||||
pageLabel: "5",
|
||||
title: "Invalid",
|
||||
createdAt: Date(timeIntervalSince1970: 300)
|
||||
)
|
||||
|
||||
let result = PDFDocumentBookmarks.clamped([invalid, older, newer], pageCount: 2)
|
||||
|
||||
XCTAssertEqual(result.map(\.id), ["newer"])
|
||||
}
|
||||
}
|
||||
24
tests/core/PDFFileSelectionTests.swift
Normal file
24
tests/core/PDFFileSelectionTests.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import XCTest
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class PDFFileSelectionTests: XCTestCase {
|
||||
func testPDFFileURLAcceptsPDFExtensionsCaseInsensitively() {
|
||||
XCTAssertTrue(PDFFileSelection.isPDFFileURL(URL(fileURLWithPath: "/tmp/article.pdf")))
|
||||
XCTAssertTrue(PDFFileSelection.isPDFFileURL(URL(fileURLWithPath: "/tmp/article.PDF")))
|
||||
}
|
||||
|
||||
func testPDFFileURLRejectsNonPDFAndRemoteURLs() {
|
||||
XCTAssertFalse(PDFFileSelection.isPDFFileURL(URL(fileURLWithPath: "/tmp/notes.txt")))
|
||||
XCTAssertFalse(PDFFileSelection.isPDFFileURL(URL(string: "https://example.com/article.pdf")!))
|
||||
}
|
||||
|
||||
func testPDFFileURLRejectsDirectoriesNamedLikePDFs() throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("pdf")
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
XCTAssertFalse(PDFFileSelection.isPDFFileURL(directory))
|
||||
}
|
||||
}
|
||||
58
tests/core/PDFRecentDocumentsTests.swift
Normal file
58
tests/core/PDFRecentDocumentsTests.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import XCTest
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class PDFRecentDocumentsTests: XCTestCase {
|
||||
func testFilteredPDFsKeepsExistingPDFsOnly() {
|
||||
let pdf = URL(fileURLWithPath: "/Users/test/Documents/reading.pdf")
|
||||
let upperPDF = URL(fileURLWithPath: "/Users/test/Documents/report.PDF")
|
||||
let text = URL(fileURLWithPath: "/Users/test/Documents/notes.txt")
|
||||
|
||||
let result = PDFRecentDocuments.filteredPDFs(
|
||||
from: [pdf, text, upperPDF],
|
||||
limit: 10,
|
||||
fileExists: { $0 == pdf || $0 == upperPDF }
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, [pdf, upperPDF])
|
||||
}
|
||||
|
||||
func testFilteredPDFsDeduplicatesExcludesCurrentAndHonorsLimit() {
|
||||
let first = URL(fileURLWithPath: "/Users/test/Documents/first.pdf")
|
||||
let second = URL(fileURLWithPath: "/Users/test/Documents/second.pdf")
|
||||
let third = URL(fileURLWithPath: "/Users/test/Documents/third.pdf")
|
||||
|
||||
let result = PDFRecentDocuments.filteredPDFs(
|
||||
from: [first, second, first, third],
|
||||
currentURL: second,
|
||||
limit: 1,
|
||||
fileExists: { _ in true }
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, [first])
|
||||
}
|
||||
|
||||
func testProgressStoresPageByNormalizedDocumentKey() {
|
||||
let url = URL(fileURLWithPath: "/Users/test/Documents/../Documents/reading.pdf")
|
||||
let openedAt = Date(timeIntervalSince1970: 42)
|
||||
|
||||
let records = PDFRecentDocuments.updatedProgress(
|
||||
[:],
|
||||
url: url,
|
||||
pageIndex: 8,
|
||||
openedAt: openedAt
|
||||
)
|
||||
let progress = PDFRecentDocuments.progress(for: url, in: records)
|
||||
|
||||
XCTAssertEqual(progress?.pageIndex, 8)
|
||||
XCTAssertEqual(progress?.openedAt, openedAt)
|
||||
XCTAssertEqual(progress?.key, PDFRecentDocuments.documentKey(for: url))
|
||||
}
|
||||
|
||||
func testProgressClampsSavedPageToAvailablePageCount() {
|
||||
XCTAssertEqual(PDFRecentDocuments.clampedPageIndex(nil, pageCount: 20), 0)
|
||||
XCTAssertEqual(PDFRecentDocuments.clampedPageIndex(-4, pageCount: 20), 0)
|
||||
XCTAssertEqual(PDFRecentDocuments.clampedPageIndex(4, pageCount: 20), 4)
|
||||
XCTAssertEqual(PDFRecentDocuments.clampedPageIndex(99, pageCount: 20), 19)
|
||||
XCTAssertEqual(PDFRecentDocuments.clampedPageIndex(4, pageCount: 0), 0)
|
||||
}
|
||||
}
|
||||
46
tests/core/PerformanceBudgetTests.swift
Normal file
46
tests/core/PerformanceBudgetTests.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import AppKit
|
||||
import PDFKit
|
||||
import XCTest
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class PerformanceBudgetTests: XCTestCase {
|
||||
func testLargeDocumentFullAnnotationSnapshotPerformance() {
|
||||
let document = makeLargeAnnotatedDocument(pageCount: 500, annotationEvery: 10)
|
||||
|
||||
measure {
|
||||
let snapshots = AnnotationReader.snapshots(in: document)
|
||||
XCTAssertEqual(snapshots.count, 50)
|
||||
}
|
||||
}
|
||||
|
||||
func testLargeDocumentPageScopedAnnotationRefreshPerformance() throws {
|
||||
let document = makeLargeAnnotatedDocument(pageCount: 500, annotationEvery: 10)
|
||||
let targetPage = try XCTUnwrap(document.page(at: 250))
|
||||
|
||||
measure {
|
||||
let snapshots = AnnotationReader.snapshots(in: document, pages: [targetPage])
|
||||
XCTAssertEqual(snapshots.count, 1)
|
||||
XCTAssertEqual(snapshots.first?.pageIndex, 250)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeLargeAnnotatedDocument(pageCount: Int, annotationEvery stride: Int) -> PDFDocument {
|
||||
let document = PDFDocument()
|
||||
|
||||
for pageIndex in 0..<pageCount {
|
||||
let page = PDFPage()
|
||||
document.insert(page, at: pageIndex)
|
||||
|
||||
guard pageIndex.isMultiple(of: stride) else { continue }
|
||||
let insertion = AnnotationFactory.noteInsertion(
|
||||
on: page,
|
||||
near: CGPoint(x: 120, y: 160),
|
||||
comment: "Note on page \(pageIndex + 1)",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(insertion.annotation)
|
||||
}
|
||||
|
||||
return document
|
||||
}
|
||||
}
|
||||
72
tests/core/ReturnKeyCommitPolicyTests.swift
Normal file
72
tests/core/ReturnKeyCommitPolicyTests.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
import XCTest
|
||||
@testable import IHatePDFsCore
|
||||
|
||||
final class ReturnKeyCommitPolicyTests: XCTestCase {
|
||||
func testPlainReturnCommitsInEditableMultilineText() {
|
||||
XCTAssertTrue(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 36,
|
||||
shift: false,
|
||||
option: false,
|
||||
command: false,
|
||||
control: false,
|
||||
isEditableMultilineText: true
|
||||
))
|
||||
}
|
||||
|
||||
func testCommandReturnCommitsWhenCommandReturnOnlyModeEnabled() {
|
||||
XCTAssertTrue(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 36,
|
||||
shift: false,
|
||||
option: false,
|
||||
command: true,
|
||||
control: false,
|
||||
isEditableMultilineText: true,
|
||||
commandReturnOnly: true
|
||||
))
|
||||
}
|
||||
|
||||
func testPlainReturnDoesNotCommitWhenCommandReturnOnlyModeEnabled() {
|
||||
XCTAssertFalse(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 36,
|
||||
shift: false,
|
||||
option: false,
|
||||
command: false,
|
||||
control: false,
|
||||
isEditableMultilineText: true,
|
||||
commandReturnOnly: true
|
||||
))
|
||||
}
|
||||
|
||||
func testKeypadEnterCommitsInEditableMultilineText() {
|
||||
XCTAssertTrue(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 76,
|
||||
shift: false,
|
||||
option: false,
|
||||
command: false,
|
||||
control: false,
|
||||
isEditableMultilineText: true
|
||||
))
|
||||
}
|
||||
|
||||
func testShiftReturnDoesNotCommitSoTextViewCanInsertNewline() {
|
||||
XCTAssertFalse(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 36,
|
||||
shift: true,
|
||||
option: false,
|
||||
command: false,
|
||||
control: false,
|
||||
isEditableMultilineText: true
|
||||
))
|
||||
}
|
||||
|
||||
func testReturnDoesNotCommitOutsideEditableMultilineText() {
|
||||
XCTAssertFalse(ReturnKeyCommitPolicy.shouldCommit(
|
||||
keyCode: 36,
|
||||
shift: false,
|
||||
option: false,
|
||||
command: false,
|
||||
control: false,
|
||||
isEditableMultilineText: false
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user