Clean up repository structure and release docs

This commit is contained in:
Akshay Kolli
2026-06-30 00:18:59 -07:00
parent 226c29b565
commit 992f1444e6
66 changed files with 330 additions and 1193 deletions

View File

@@ -0,0 +1,290 @@
import CoreGraphics
import Foundation
import IHatePDFsCore
import PDFKit
import XCTest
@testable import IHatePDFs
@MainActor
final class AppStateWorkflowTests: XCTestCase {
func testOpeningDocumentStartsInFocusedReadingWorkflow() throws {
let url = try makeTemporaryPDF()
defer { try? FileManager.default.removeItem(at: url) }
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.showLeftSidebar = true
appState.leftSidebarMode = .annotations
appState.showCommentsSidebar = true
appState.sidebarMode = .highlights
appState.commentSearchText = "draft"
appState.commentFilter = .withComments
appState.selectedKindFilter = .comment
appState.selectedAuthorFilter = "Someone"
appState.selectedStatusFilter = ReviewState.reviewed
appState.collapsedPageIndexes = [0]
appState.loadDocument(from: url)
XCTAssertNotNil(appState.document)
XCTAssertEqual(appState.documentURL, url)
XCTAssertFalse(appState.showLeftSidebar)
XCTAssertEqual(appState.leftSidebarMode, .pages)
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
XCTAssertEqual(appState.commentSearchText, "")
XCTAssertEqual(appState.commentFilter, .all)
XCTAssertNil(appState.selectedKindFilter)
XCTAssertEqual(appState.selectedAuthorFilter, "All Authors")
XCTAssertEqual(appState.selectedStatusFilter, ReviewState.allStatuses)
XCTAssertTrue(appState.collapsedPageIndexes.isEmpty)
XCTAssertFalse(appState.hasUnsavedChanges)
}
func testOpeningDocumentCollapsesSidebars() throws {
let url = try makeTemporaryPDF()
defer { try? FileManager.default.removeItem(at: url) }
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.showLeftSidebar = true
appState.leftSidebarMode = .annotations
appState.showCommentsSidebar = true
appState.sidebarMode = .highlights
appState.loadDocument(from: url)
XCTAssertFalse(appState.showLeftSidebar)
XCTAssertFalse(appState.showCommentsSidebar)
}
func testDroppingPDFWhileDocumentOpenReplacesThroughAppState() async throws {
let firstURL = try makeTemporaryPDF()
let secondURL = try makeTemporaryPDF()
defer {
try? FileManager.default.removeItem(at: firstURL)
try? FileManager.default.removeItem(at: secondURL)
}
let appState = AppState()
appState.loadDocument(from: firstURL)
XCTAssertEqual(appState.documentURL, firstURL)
let provider = try XCTUnwrap(NSItemProvider(contentsOf: secondURL))
XCTAssertTrue(appState.openDroppedDocument(from: [provider]))
try await waitUntil {
appState.documentURL == secondURL
}
XCTAssertEqual(appState.documentURL, secondURL)
XCTAssertNotNil(appState.document)
XCTAssertFalse(appState.hasUnsavedChanges)
}
func testClosingDocumentReturnsToEmptyWindowWorkflow() throws {
let url = try makeTemporaryPDF()
defer { try? FileManager.default.removeItem(at: url) }
let appState = AppState()
appState.loadDocument(from: url)
appState.showLeftSidebar = true
appState.leftSidebarMode = .annotations
appState.showCommentsSidebar = true
appState.sidebarMode = .highlights
appState.searchText = "draft"
appState.showToolbarSearch = true
appState.collapsedPageIndexes = [0]
appState.closeDocument()
XCTAssertNil(appState.document)
XCTAssertNil(appState.documentURL)
XCTAssertFalse(appState.showLeftSidebar)
XCTAssertEqual(appState.leftSidebarMode, .pages)
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
XCTAssertEqual(appState.searchText, "")
XCTAssertFalse(appState.showToolbarSearch)
XCTAssertTrue(appState.annotations.isEmpty)
XCTAssertTrue(appState.bookmarks.isEmpty)
XCTAssertEqual(appState.currentPageIndex, 0)
XCTAssertEqual(appState.pageText, "1")
XCTAssertFalse(appState.hasUnsavedWork)
XCTAssertEqual(appState.statusMessage, "Closed PDF.")
}
func testCompactWorkflowShowsOnlyOneSidebarAtATime() {
let appState = AppState()
appState.updateWindowWidth(ReaderAdaptiveLayout.minimumWindowWidth)
appState.toggleRightSidebar(mode: .highlights)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
XCTAssertFalse(appState.showLeftSidebar)
appState.togglePageSidebar()
XCTAssertTrue(appState.showLeftSidebar)
XCTAssertEqual(appState.leftSidebarMode, .pages)
XCTAssertFalse(appState.showCommentsSidebar)
appState.toggleRightSidebar(mode: .highlights)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
XCTAssertFalse(appState.showLeftSidebar)
}
func testPageSidebarToggleClosesLeftSidebarEvenWhenMarksAreSelected() {
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.showLeftSidebar = true
appState.leftSidebarMode = .annotations
appState.togglePageSidebar()
XCTAssertFalse(appState.showLeftSidebar)
XCTAssertEqual(appState.leftSidebarMode, .annotations)
}
func testRightSidebarToolbarToggleClosesAndReopensCurrentMode() {
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.toggleRightSidebar(mode: .highlights)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebarVisibility()
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebarVisibility()
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
}
func testRightSidebarToggleClosesFromDifferentOpenMode() {
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.toggleRightSidebar(mode: .highlights)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebar(mode: .annotations)
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebar(mode: .annotations)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
}
func testRightSidebarToggleFromHighlightsDoesNotSwitchModeWhenClosing() {
let appState = AppState()
appState.updateWindowWidth(1_280)
appState.toggleRightSidebar(mode: .highlights)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebarVisibility()
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
appState.toggleRightSidebarVisibility()
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .highlights)
}
func testRightSidebarToolbarToggleDefaultsToCommentsWhenNoModeWasChosen() {
let appState = AppState()
appState.updateWindowWidth(1_280)
XCTAssertFalse(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
appState.toggleRightSidebarVisibility()
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
}
func testRegularWorkflowAllowsNavigationAndReviewSidebarsTogether() {
let appState = AppState()
appState.updateWindowWidth(1_000)
appState.togglePageSidebar()
appState.toggleRightSidebar(mode: .annotations)
XCTAssertTrue(appState.showLeftSidebar)
XCTAssertEqual(appState.leftSidebarMode, .pages)
XCTAssertTrue(appState.showCommentsSidebar)
XCTAssertEqual(appState.sidebarMode, .annotations)
}
func testSaveAvailabilityTracksReplyDraftAsUnsavedWork() throws {
let url = try makeTemporaryPDF()
defer { try? FileManager.default.removeItem(at: url) }
let appState = AppState()
appState.loadDocument(from: url)
XCTAssertFalse(appState.hasUnsavedWork)
XCTAssertFalse(appState.canSaveDocument)
XCTAssertEqual(appState.saveHelpText, "No unsaved changes.")
appState.sidebarReplyDraft = "Need to verify this quote."
XCTAssertTrue(appState.hasUnsavedWork)
XCTAssertTrue(appState.hasUnsentSidebarReplyDraft)
XCTAssertTrue(appState.canSaveDocument)
XCTAssertEqual(appState.saveHelpText, "Send or cancel the reply draft before saving.")
appState.hasUnsavedChanges = true
XCTAssertTrue(appState.canSaveDocument)
XCTAssertEqual(appState.saveHelpText, "Save PDF")
}
private func makeTemporaryPDF() throws -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("pdf")
let data = NSMutableData()
guard let consumer = CGDataConsumer(data: data) else {
throw TestPDFError.couldNotCreateConsumer
}
var mediaBox = CGRect(x: 0, y: 0, width: 612, height: 792)
guard let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
throw TestPDFError.couldNotCreateContext
}
context.beginPDFPage(nil)
context.endPDFPage()
context.closePDF()
try data.write(to: url, options: .atomic)
return url
}
private enum TestPDFError: Error {
case couldNotCreateConsumer
case couldNotCreateContext
}
private func waitUntil(
timeout: TimeInterval = 2,
condition: @MainActor @escaping () -> Bool
) async throws {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() {
return
}
try await Task.sleep(nanoseconds: 20_000_000)
}
XCTFail("Timed out waiting for condition.")
}
}

View File

@@ -0,0 +1,122 @@
import XCTest
@testable import IHatePDFs
final class ReaderAdaptiveLayoutTests: XCTestCase {
func testSizeClassBreakpointsMatchMacWindowProfiles() {
XCTAssertEqual(ReaderAdaptiveLayout(width: 759).sizeClass, .compact)
XCTAssertEqual(ReaderAdaptiveLayout(width: 959).sizeClass, .compact)
XCTAssertEqual(ReaderAdaptiveLayout(width: 960).sizeClass, .regular)
XCTAssertEqual(ReaderAdaptiveLayout(width: 1_279).sizeClass, .regular)
XCTAssertEqual(ReaderAdaptiveLayout(width: 1_280).sizeClass, .wide)
}
func testCompactProfileKeepsSingleSidebarAndDocumentReadableAtMinimumWidth() {
let layout = ReaderAdaptiveLayout(width: ReaderAdaptiveLayout.minimumWindowWidth)
XCTAssertEqual(layout.sizeClass, .compact)
XCTAssertFalse(layout.allowsDualSidebars)
XCTAssertTrue(layout.usesCompactToolbar)
assertDocumentWidthIsPreserved(
layout: layout,
availableWidth: ReaderAdaptiveLayout.minimumWindowWidth,
requestedLeft: layout.leftSidebarMaxWidth,
requestedRight: 0,
showLeft: true,
showRight: false
)
assertDocumentWidthIsPreserved(
layout: layout,
availableWidth: ReaderAdaptiveLayout.minimumWindowWidth,
requestedLeft: 0,
requestedRight: layout.rightSidebarMaxWidth,
showLeft: false,
showRight: true
)
}
func testRegularProfilePreservesDocumentWidthWithBothSidebarsAtBreakpoint() {
let layout = ReaderAdaptiveLayout(width: 960)
XCTAssertEqual(layout.sizeClass, .regular)
XCTAssertTrue(layout.allowsDualSidebars)
XCTAssertFalse(layout.usesCompactToolbar)
assertDocumentWidthIsPreserved(
layout: layout,
availableWidth: 960,
requestedLeft: layout.leftSidebarIdealWidth,
requestedRight: layout.rightSidebarIdealWidth,
showLeft: true,
showRight: true
)
}
func testExpandedRegularSidebarsShrinkBeforeTheDocumentDoes() {
let layout = ReaderAdaptiveLayout(width: 960)
let widths = layout.resolvedSidebarWidths(
availableWidth: 960,
requestedLeft: layout.leftSidebarMaxWidth,
requestedRight: layout.rightSidebarMaxWidth,
showLeft: true,
showRight: true
)
XCTAssertGreaterThanOrEqual(widths.left, layout.leftSidebarMinWidth - 0.001)
XCTAssertGreaterThanOrEqual(widths.right, layout.rightSidebarMinWidth - 0.001)
XCTAssertLessThanOrEqual(widths.left, layout.leftSidebarMaxWidth + 0.001)
XCTAssertLessThanOrEqual(widths.right, layout.rightSidebarMaxWidth + 0.001)
assertDocumentWidthIsPreserved(
layout: layout,
availableWidth: 960,
requestedLeft: layout.leftSidebarMaxWidth,
requestedRight: layout.rightSidebarMaxWidth,
showLeft: true,
showRight: true
)
}
func testWideProfileUsesRoomierSidebarsWhilePreservingDocumentWidth() {
let layout = ReaderAdaptiveLayout(width: 1_280)
XCTAssertEqual(layout.sizeClass, .wide)
XCTAssertGreaterThan(layout.rightSidebarIdealWidth, ReaderAdaptiveLayout(width: 960).rightSidebarIdealWidth)
XCTAssertGreaterThan(layout.documentMinWidth, ReaderAdaptiveLayout(width: 960).documentMinWidth)
assertDocumentWidthIsPreserved(
layout: layout,
availableWidth: 1_280,
requestedLeft: layout.leftSidebarIdealWidth,
requestedRight: layout.rightSidebarIdealWidth,
showLeft: true,
showRight: true
)
}
private func assertDocumentWidthIsPreserved(
layout: ReaderAdaptiveLayout,
availableWidth: CGFloat,
requestedLeft: CGFloat,
requestedRight: CGFloat,
showLeft: Bool,
showRight: Bool,
file: StaticString = #filePath,
line: UInt = #line
) {
let widths = layout.resolvedSidebarWidths(
availableWidth: availableWidth,
requestedLeft: requestedLeft,
requestedRight: requestedRight,
showLeft: showLeft,
showRight: showRight
)
let documentWidth = layout.visibleContentWidth(
availableWidth: availableWidth,
leftWidth: widths.left,
rightWidth: widths.right,
showLeft: showLeft,
showRight: showRight
)
XCTAssertGreaterThanOrEqual(documentWidth, layout.documentMinWidth - 0.001, file: file, line: line)
}
}

View 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)
}
}

View 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)
}
}

View 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))
}
}

View 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"])
}
}

View 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))
}
}

View 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)
}
}

View 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
}
}

View 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
))
}
}