| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import XCTest |
| 4 | @testable import DocsKit |
| 5 | |
| 6 | final class DocumentTypographyTests: XCTestCase { |
| 7 | // Synthetic fixture also measured in WebKit at 96 CSS px/in with the same |
| 8 | // bundled Liberation fonts and the production [docs-text] CSS rules. |
| 9 | @MainActor func testBrowserFixtureLineBreaks() throws { |
| 10 | let heading = "A shared document with consistent typography on every screen" |
| 11 | let bold = "This is a synthetic layout fixture for the native and browser editors. The same bundled font, page width and paragraph styles must produce the same line breaks, including a long sentence that wraps across several lines." |
| 12 | let body = "The quick brown fox jumps over the lazy dog. A small change in font metrics can move the final word onto another line, so this paragraph deliberately includes enough words to exercise wrapping at a fixed document width." |
| 13 | func block(_ id: String, _ text: String, heading: Bool = false, strong: Bool = false) -> CRDTNode { |
| 14 | CRDTNode(id: id, type: heading ? "heading" : "paragraph", attrs: heading ? ["level": "1"] : nil, children: [CRDTNode(id: id + "t", type: "text", runs: [CRDTRun(insert: text, marks: strong ? ["strong": .object([:])] : nil)])]) |
| 15 | } |
| 16 | let projection = CRDTProjection(children: [block("h", heading, heading: true), block("b", bold, strong: true), block("gap", ""), block("p", body)], canUndo: false, canRedo: false) |
| 17 | let storage = NSTextStorage(attributedString: NativeTextProjection.render(projection)) |
| 18 | let manager = NSLayoutManager() |
| 19 | manager.usesFontLeading = false |
| 20 | let container = DocsPageTextContainer(size: CGSize(width: 636, height: 100000)) |
| 21 | container.geometry = DocsPageGeometry(viewportWidth: 636, paged: false) |
| 22 | container.lineFragmentPadding = 0 |
| 23 | storage.addLayoutManager(manager); manager.addTextContainer(container) |
| 24 | manager.ensureLayout(for: container) |
| 25 | var lines: [String] = [] |
| 26 | var heights: [CGFloat] = [] |
| 27 | manager.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: manager.numberOfGlyphs)) { rect, _, _, range, _ in |
| 28 | let chars = manager.characterRange(forGlyphRange: range, actualGlyphRange: nil) |
| 29 | let line = (storage.string as NSString).substring(with: chars).trimmingCharacters(in: .whitespacesAndNewlines) |
| 30 | if !line.isEmpty { lines.append(line); heights.append(rect.height) } |
| 31 | } |
| 32 | XCTAssertEqual(heights[2], 16.8, accuracy: 0.01) |
| 33 | XCTAssertEqual(heights[3], 16.8, accuracy: 0.01) |
| 34 | XCTAssertEqual(lines, Self.browserLines) |
| 35 | } |
| 36 | private static let browserLines: [String] = [ |
| 37 | "A shared document with consistent typography on", |
| 38 | "every screen", |
| 39 | "This is a synthetic layout fixture for the native and browser editors. The same", |
| 40 | "bundled font, page width and paragraph styles must produce the same line", |
| 41 | "breaks, including a long sentence that wraps across several lines.", |
| 42 | "The quick brown fox jumps over the lazy dog. A small change in font metrics can move", |
| 43 | "the final word onto another line, so this paragraph deliberately includes enough words to", |
| 44 | "exercise wrapping at a fixed document width." |
| 45 | ] |
| 46 | } |
| 47 | #endif |
| 48 | |