| 1 | import Foundation |
| 2 | import RastrilloApple |
| 3 | import XCTest |
| 4 | @testable import DocsKit |
| 5 | |
| 6 | final class DocumentToolbarSelectionTests: XCTestCase { |
| 7 | @MainActor func testTypingKeepsPresentationAvailableWhileCommandsWaitForProjection() async throws { |
| 8 | let model = DocsModel(client: NativeHTTP(origin: URL(string: "https://example.invalid")!)) |
| 9 | model.detail = DocumentDetail(sqid: "fixture", title: "Fixture", guid: "fixture", can_write: true, can_share: false) |
| 10 | model.projection = CRDTProjection(children: [], canUndo: true, canRedo: false) |
| 11 | let content = NSMutableAttributedString(string: "bold\n") |
| 12 | content.addAttribute(.docsMarks, value: try JSONEncoder().encode(["strong": JSONValue.object([:])]), range: NSRange(location: 0, length: 4)) |
| 13 | model.selection = NSRange(location: 4, length: 0) |
| 14 | let commands = DocumentCommands(model: model, content: content) |
| 15 | let settled = try XCTUnwrap(DocumentToolbarSelection(commands: commands)) |
| 16 | XCTAssertEqual(settled.marks, ["strong"]) |
| 17 | XCTAssertTrue(settled.canUndo) |
| 18 | |
| 19 | // Native input advances the caret before the replica publishes its text. |
| 20 | XCTAssertTrue(model.enqueue(["op": .string("display_edit"), "at": .number(4), "length": .number(0), "text": .string("!!")])) |
| 21 | model.selection = NSRange(location: 6, length: 0) |
| 22 | XCTAssertTrue(model.hasPendingInput) |
| 23 | XCTAssertTrue(commands.isAvailable, "Transient input must not dim the whole toolbar") |
| 24 | XCTAssertFalse(commands.isEnabled) |
| 25 | XCTAssertNil(DocumentToolbarSelection(commands: commands), "Do not replace the bold highlight using stale text") |
| 26 | XCTAssertFalse(commands.format("em"), "Formatting must still reject an unacknowledged selection") |
| 27 | XCTAssertNil(model.typingMarks) |
| 28 | await model.discardAll() |
| 29 | |
| 30 | // A new projection still needs the native view's acknowledgement. |
| 31 | model.detail = DocumentDetail(sqid: "fixture", title: "Fixture", guid: "fixture", can_write: true, can_share: false) |
| 32 | model.projection = CRDTProjection(children: [], canUndo: true, canRedo: false) |
| 33 | model.projectionGeneration = 2 |
| 34 | model.selection = NSRange(location: 4, length: 0) |
| 35 | XCTAssertTrue(commands.isAvailable) |
| 36 | XCTAssertNil(DocumentToolbarSelection(commands: commands)) |
| 37 | model.didRender(2) |
| 38 | XCTAssertEqual(DocumentToolbarSelection(commands: commands), settled) |
| 39 | model.selection = NSRange(location: 0, length: 2) |
| 40 | XCTAssertTrue(try XCTUnwrap(DocumentToolbarSelection(commands: commands)).hasSelection) |
| 41 | } |
| 42 | |
| 43 | @MainActor func testToolbarLabelsFollowHeadingFontSizeAndAlignment() throws { |
| 44 | let model = DocsModel(client: NativeHTTP(origin: URL(string: "https://example.invalid")!)) |
| 45 | model.detail = DocumentDetail(sqid: "fixture", title: "Fixture", guid: "fixture", can_write: true, can_share: false) |
| 46 | model.projection = CRDTProjection(children: [CRDTNode(id: "heading", type: "heading", attrs: ["level": "2", "align": "center"], children: [CRDTNode(id: "text", type: "text", runs: [CRDTRun(insert: "Title", marks: ["font_serif": .object([:]), "size_24": .object([:])])])])], canUndo: false, canRedo: false) |
| 47 | model.selection = NSRange(location: 1, length: 0) |
| 48 | let state = try XCTUnwrap(DocumentToolbarSelection(commands: DocumentCommands(model: model, content: NativeTextProjection.render(model.projection!)))) |
| 49 | XCTAssertEqual(state.style, "heading2") |
| 50 | XCTAssertEqual(state.font, "serif") |
| 51 | XCTAssertEqual(state.size, 24) |
| 52 | XCTAssertEqual(state.alignment, "center") |
| 53 | for level in 4...6 { |
| 54 | model.projection?.children[0].attrs?["level"] = String(level) |
| 55 | let imported = try XCTUnwrap(DocumentToolbarSelection(commands: DocumentCommands(model: model, content: NativeTextProjection.render(model.projection!)))) |
| 56 | XCTAssertEqual(imported.styleLabel, String(format: docsMacString("Heading %d"), level)) |
| 57 | } |
| 58 | for (mark, expected) in [("size_sm", 10), ("size_lg", 14), ("size_xl", 18)] { |
| 59 | model.typingMarks = [mark: .object([:])] |
| 60 | let legacy = try XCTUnwrap(DocumentToolbarSelection(commands: DocumentCommands(model: model, content: NativeTextProjection.render(model.projection!)))) |
| 61 | XCTAssertEqual(legacy.size, expected) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | @MainActor func testReadOnlyAndPausedInputRemainDisabled() { |
| 66 | let model = DocsModel(client: NativeHTTP(origin: URL(string: "https://example.invalid")!)) |
| 67 | let commands = DocumentCommands(model: model, content: NSAttributedString(string: "")) |
| 68 | model.detail = DocumentDetail(sqid: "fixture", title: "Fixture", guid: "fixture", can_write: false, can_share: false) |
| 69 | XCTAssertFalse(commands.isAvailable) |
| 70 | XCTAssertFalse(commands.format("strong")) |
| 71 | model.detail = DocumentDetail(sqid: "fixture", title: "Fixture", guid: "fixture", can_write: true, can_share: false) |
| 72 | model.inputPaused = true |
| 73 | XCTAssertFalse(commands.isAvailable) |
| 74 | XCTAssertFalse(commands.format("strong")) |
| 75 | model.inputPaused = false |
| 76 | model.composing = true |
| 77 | XCTAssertTrue(commands.isAvailable) |
| 78 | XCTAssertFalse(commands.isEnabled) |
| 79 | XCTAssertNil(DocumentToolbarSelection(commands: commands)) |
| 80 | XCTAssertFalse(commands.format("strong")) |
| 81 | } |
| 82 | } |
| 83 | |