import Foundation import XCTest @testable import DocsKit final class DocumentCommandsTests: XCTestCase { @MainActor func testTypingInheritsPreviousTextButDoesNotExtendLinksOrCrossParagraphs() throws { let content = NSMutableAttributedString(string: "boldplain\nnext\n") let strong: [String: JSONValue] = ["strong": .object([:])] let link: [String: JSONValue] = ["link": .object(["href": .string("https://example.com")])] content.addAttribute(.docsMarks, value: try JSONEncoder().encode(strong), range: NSRange(location: 0, length: 4)) content.addAttribute(.docsMarks, value: try JSONEncoder().encode([String: JSONValue]()), range: NSRange(location: 4, length: 5)) content.addAttribute(.docsMarks, value: try JSONEncoder().encode(link), range: NSRange(location: 10, length: 4)) XCTAssertEqual(TextEditMapping.marks(in: content, at: 4), strong) XCTAssertEqual(TextEditMapping.marks(in: content, at: 4, preferFollowing: true), [:]) XCTAssertEqual(TextEditMapping.marks(in: content, at: 10), [:]) XCTAssertEqual(TextEditMapping.marks(in: content, at: 12), link) XCTAssertEqual(TextEditMapping.marks(in: content, at: 14), [:]) let onlyBold = NSMutableAttributedString(string: "bold\n") onlyBold.addAttribute(.docsMarks, value: try JSONEncoder().encode(strong), range: NSRange(location: 0, length: 4)) XCTAssertEqual(TextEditMapping.displayCommand(in: onlyBold, range: NSRange(location: 4, length: 0), replacement: "!")?["marks"], .object(strong)) } @MainActor func testListConversionsTargetEachContainerOnceAndLiftFromEnd() { let list = CRDTNode(id: "list", type: "bullet_list") let first = CRDTNode(id: "first", type: "list_item") let second = CRDTNode(id: "second", type: "list_item") let paths = [ [list, first, CRDTNode(id: "a", type: "paragraph")], [list, first, CRDTNode(id: "b", type: "paragraph")], [list, second, CRDTNode(id: "c", type: "paragraph")], ] let converted = DocumentCommands.listCommands(type: "ordered_list", paths: paths) XCTAssertEqual(converted.count, 1) XCTAssertEqual(converted.first?["node"], .string("c")) let lifted = DocumentCommands.listCommands(type: "bullet_list", paths: paths) XCTAssertEqual(lifted.map { $0["node"] }, [.string("c"), .string("b")]) } @MainActor func testFormattingAcrossBlocksKeepsUTF16OffsetsAndSkipsAttachments() throws { let content = NSMutableAttributedString(string: "a😀b\ncd\n\u{fffc}") let marks = try JSONEncoder().encode([String: JSONValue]()) content.addAttributes([.docsNode: "first", .docsMarks: marks], range: NSRange(location: 0, length: 4)) content.addAttributes([.docsNode: "second", .docsMarks: marks], range: NSRange(location: 5, length: 2)) content.addAttribute(.docsNode, value: "image", range: NSRange(location: 8, length: 1)) let commands = DocumentCommands.markCommands(content: content, selection: NSRange(location: 1, length: 8), marks: ["strong": .object([:])]) XCTAssertEqual(commands.count, 2) XCTAssertEqual(commands[0]["node"], .string("first")) XCTAssertEqual(commands[0]["at"], .number(1)) XCTAssertEqual(commands[0]["length"], .number(3)) XCTAssertEqual(commands[1]["node"], .string("second")) XCTAssertEqual(commands[1]["at"], .number(0)) XCTAssertEqual(commands[1]["length"], .number(2)) XCTAssertEqual(commands[1]["op"], .string("format")) XCTAssertNil(commands[1]["text"]) } @MainActor func testExportNamesUseDocumentTitleWithoutPathOrOversizeBytes() { XCTAssertEqual(DocumentExportControls.filename(title: "Quarterly report", extension: "pdf"), "Quarterly report.pdf") XCTAssertEqual(DocumentExportControls.filename(title: "Notes.md", extension: "md"), "Notes.md") let name = DocumentExportControls.filename(title: "../team:notes\\draft\n" + String(repeating: "日本語", count: 100), extension: "md") XCTAssertFalse(name.contains("/")); XCTAssertFalse(name.contains("\\")); XCTAssertFalse(name.contains(":")); XCTAssertFalse(name.contains("\n")) XCTAssertLessThanOrEqual(name.utf8.count, 183) XCTAssertTrue(name.hasSuffix(".md")) } }