Prepare v0.4 release and open source docs

This commit is contained in:
Akshay Kolli
2026-06-29 23:42:39 -07:00
parent 085d7a16dc
commit 504bd2d39a
58 changed files with 5076 additions and 923 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)
}
}