Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

97 lines
6.4 KiB
Swift

#if canImport(UIKit)
import XCTest
import RichTextEditorCore
@testable import RichTextEditorUIKit
final class BlockQuoteGeometryTests: XCTestCase {
// Follow the QuoteVerticalInsetTests helper pattern: setBlocks then set frame + layoutIfNeeded so
// BlockStack.layout has run and all box.frame values are populated.
private func canvas(_ blocks: [Block], width: CGFloat = 320) -> DocumentCanvasView {
let c = DocumentCanvasView()
c.setBlocks(blocks, width: width)
c.frame = CGRect(x: 0, y: 0, width: width, height: 600)
c.layoutIfNeeded()
return c
}
/// Two adjacent top-level block quotes must NOT sit flush — there must be a vertical gap between
/// their frames (separation gap via `isFramedAtom` + external-gap insertion in `BlockStack.layout`).
func test_twoAdjacentBlockQuotes_haveSeparationGap() {
let a = BlockQuote(id: BlockID("a"), children: [.paragraph(ParagraphBlock(id: BlockID("pa"), runs: [TextRun(text:"a")]))], collapsed:false)
let b = BlockQuote(id: BlockID("b"), children: [.paragraph(ParagraphBlock(id: BlockID("pb"), runs: [TextRun(text:"b")]))], collapsed:false)
let c = canvas([.blockQuote(a), .blockQuote(b)])
let boxA = c.boxes[0], boxB = c.boxes[1]
XCTAssertGreaterThan(boxB.frame.minY - boxA.frame.maxY, 0,
"Two adjacent BlockQuoteBoxes must have a positive separation gap, not sit flush")
}
/// Bug 2 regression: the expanded quote's first child's content-top must equal `box.frame.minY +
/// box.topInset` — the same Y the collapsed preview starts at. Before the fix,
/// `stack.verticalInsetBase == 8` added an extra 8pt top gap to the first child (via
/// `facingInset(toward: nil) == base`), so expanded content sat 8pt lower than the collapsed preview.
func test_expandedFirstChild_contentTop_equalsTopInset() {
let bq = BlockQuote(id: BlockID("q"), children: [
.paragraph(ParagraphBlock(id: BlockID("p"), runs: [TextRun(text: "content")]))
], collapsed: false)
let c = canvas([.blockQuote(bq)])
let box = c.boxes[0] as! BlockQuoteBox
let childBox = box.children.boxes[0]
// The child's frame.minY is its top relative to the canvas; box.frame.minY + box.topInset
// is where the collapsed preview text would start.
let expectedContentTop = box.frame.minY + box.topInset
XCTAssertEqual(childBox.frame.minY, expectedContentTop, accuracy: 0.5,
"Expanded first child's top must align with topInset (== collapsed preview top). " +
"Extra gap means verticalInsetBase is non-zero.")
}
/// Collapsed vs expanded height parity for a single-line body-paragraph quote.
/// Before the original fix, `previewAttributes` used a plain NSMutableParagraphStyle (no
/// lineHeightMultiple), making the collapsed box ~2pt shorter than the expanded one for the same content.
///
/// Task 4 update: the expanded box now ALSO reserves an always-present author-line region below its
/// child stack (bold caption, showing an "Add author" placeholder when empty) — the collapsed folded
/// preview never shows one (`DocumentTree` maps a collapsed quote to a caption-less atom, entirely off
/// the position axis; collapsing the quote loses the author's ON-CANVAS rendering, not the underlying
/// model field). So the two TOTAL heights are no longer expected to be equal. The original invariant
/// this test guarded — the child stack's CONTENT height (topInset + children + bottomInset) matches the
/// collapsed preview's height for identical single-line body content — still holds; verify it directly,
/// independent of the (now additive) author-line reservation, and separately assert the new inequality.
func test_collapsedVsExpanded_singleLineBodyChild_heightParity() {
let text = "Hello world"
let paraBlock = ParagraphBlock(id: BlockID("p"), style: .body, runs: [TextRun(text: text)])
let expandedBQ = BlockQuote(id: BlockID("bq-e"), children: [.paragraph(paraBlock)], collapsed: false)
let collapsedBQ = BlockQuote(id: BlockID("bq-c"), children: [.paragraph(paraBlock)], collapsed: true)
let width: CGFloat = 320
let expandedCanvas = canvas([.blockQuote(expandedBQ)], width: width)
let collapsedCanvas = canvas([.blockQuote(collapsedBQ)], width: width)
let expandedBox = expandedCanvas.boxes[0] as! BlockQuoteBox
let collapsedHeight = collapsedCanvas.boxes[0].height
let innerWidth = max(width - expandedBox.quoteStyle.leadingInset - expandedBox.quoteStyle.trailingInset, 1)
let contentHeight = expandedBox.topInset + expandedBox.children.measuredHeight(forWidth: innerWidth) + expandedBox.bottomInset
XCTAssertEqual(contentHeight, collapsedHeight, accuracy: 0.5,
"Collapsed and expanded BlockQuoteBox must have equal CONTENT height (excluding the " +
"author-line reservation) for identical single-line body content. " +
"Content: \(contentHeight), Collapsed: \(collapsedHeight)")
XCTAssertGreaterThan(expandedBox.height, collapsedHeight,
"Expanded is now taller than collapsed by the always-reserved (placeholder) author-line region")
}
/// A nested block quote must receive its OWN fill rect from the recursive `blockQuoteFillRects()`
/// collector, not just the outer fill.
func test_nestedBlockQuote_fillIsEmitted() {
let inner = BlockQuote(id: BlockID("in"), children: [.paragraph(ParagraphBlock(id: BlockID("ip"), runs:[TextRun(text:"z")]))], collapsed:false)
let outer = BlockQuote(id: BlockID("out"), children: [.blockQuote(inner)], collapsed:false)
let c = canvas([.blockQuote(outer)])
let fills = c.blockQuoteFillRects()
XCTAssertGreaterThanOrEqual(fills.count, 2,
"blockQuoteFillRects() must emit at least one fill per BlockQuoteBox level (outer + inner)")
// The outer fill must be larger than the inner fill (the inner is nested inside).
if fills.count >= 2 {
let outerFill = fills[0], innerFill = fills[1]
XCTAssertGreaterThan(outerFill.height, innerFill.height,
"Outer fill should be taller than the nested inner fill")
}
}
}
#endif