import Foundation import XCTest #if os(macOS) import AppKit #endif @testable import DocsKit final class DocumentRenderReconciliationTests: XCTestCase { @MainActor func testAcknowledgedOptimisticTextUpdatesMappingWithoutReplacingCharacters() { #if os(macOS) let storage = NSTextStorage(string: "Hello 😀\n", attributes: [.docsNode: "old", .docsOffset: 0, .font: NSFont.systemFont(ofSize: 12)]) let observer = StorageObserver() storage.delegate = observer let next = NSAttributedString(string: storage.string, attributes: [.docsNode: "published", .docsOffset: 7, .font: NSFont.systemFont(ofSize: 12)]) DocumentRenderReconciliation.apply(next, to: storage) XCTAssertEqual(storage.string, next.string) XCTAssertEqual(storage.attribute(.docsNode, at: 0, effectiveRange: nil) as? String, "published") XCTAssertEqual(storage.attribute(.docsOffset, at: 0, effectiveRange: nil) as? Int, 7) XCTAssertFalse(observer.editedCharacters) XCTAssertTrue(observer.editedAttributes) #endif } @MainActor func testRemoteReplacementPreservesUnicodeAndAllAttributes() { let storage = NSMutableAttributedString(string: "A😀old Z", attributes: [.docsNode: "old"]) let next = NSMutableAttributedString(string: "A😁日本語 Z", attributes: [.docsNode: "new"]) next.addAttribute(.docsOffset, value: 12, range: NSRange(location: 3, length: 3)) DocumentRenderReconciliation.apply(next, to: storage) XCTAssertTrue(storage.isEqual(to: next)) } #if os(macOS) @MainActor func testMacLogicalPaperWidthDoesNotReflowAtNarrowZoomedViewport() { let narrow = DocsPageGeometry(viewportWidth: 350, paged: true) let wide = DocsPageGeometry(viewportWidth: 1200, paged: true) XCTAssertEqual(narrow.textWidth, 468) XCTAssertEqual(wide.textWidth, narrow.textWidth) XCTAssertEqual(narrow.paperWidth, 612) } #endif } #if os(macOS) @MainActor private final class StorageObserver: NSObject, @preconcurrency NSTextStorageDelegate { var editedCharacters = false var editedAttributes = false func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) { editedCharacters = editedCharacters || editedMask.contains(.editedCharacters) editedAttributes = editedAttributes || editedMask.contains(.editedAttributes) } } #endif