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

102 lines
5.8 KiB
Swift

import XCTest
import UIKit
@testable import ChatMessageDateAndStatusNode
/// The status node reserves vertical space for the reaction buttons during the measure pass and
/// positions them during the apply pass. Those were two separate copies of one row-packing loop,
/// run against two different widths — `arguments.constrainedSize.width` when measuring and the
/// `boundingWidth` handed to the continue closure when applying — so they could disagree on the row
/// count and the node reserved rows it never drew.
///
/// The symptom was a rich-text message in a channel: the bubble is widened past the content node's
/// proposal by the author-name header, so two reactions packed onto two rows when measured and onto
/// one row when drawn, leaving a blank reaction row of dead space above the date.
///
/// `packReactionRows` is now the single routine both passes go through. These tests pin the property
/// that made the duplication a bug in the first place: **the reported size must be the true extent
/// of the returned positions**, so a caller reserving `size.height` reserves exactly what gets drawn.
final class ReactionRowPackingTests: XCTestCase {
private let pill = CGSize(width: 50.0, height: 30.0)
private let topInset: CGFloat = 3.0
private func sizes(_ count: Int) -> [CGSize] {
return Array(repeating: self.pill, count: count)
}
/// The invariant the two-copy version violated: whatever width it is packed at, the reported
/// height is exactly the extent of the positions it returns.
func test_reportedHeight_isTheExtentOfTheReturnedPositions() {
for count in 1 ... 6 {
for width in stride(from: 20.0, through: 400.0, by: 7.0) {
let pack = packReactionRows(self.sizes(count), width: width, topInset: self.topInset)
XCTAssertEqual(pack.positions.count, count, "every item is placed (count \(count), width \(width))")
let bottom = pack.positions.map { $0.y + self.pill.height }.max() ?? self.topInset
XCTAssertEqual(pack.size.height, bottom - self.topInset, accuracy: 0.001,
"reserved height vs drawn extent (count \(count), width \(width))")
}
}
}
/// Rows are `reactionButtonSpacing` apart and each starts at the leading bleed.
func test_rowsAreEvenlyPitchedAndLeadingAligned() {
let pack = packReactionRows(self.sizes(4), width: 110.0, topInset: self.topInset)
let rowYs = Array(Set(pack.positions.map { $0.y })).sorted()
XCTAssertEqual(rowYs.count, 2, "two pills per row at this width")
XCTAssertEqual(rowYs[1] - rowYs[0], self.pill.height + reactionButtonSpacing, accuracy: 0.001)
for y in rowYs {
let first = pack.positions.first(where: { $0.y == y })
XCTAssertEqual(first?.x, reactionButtonLeadingBleed, "each row starts at the bleed")
}
}
/// The reported width is the widest row, so a caller can propose a bubble wide enough to hold it.
func test_reportedWidth_isTheWidestRow() {
let pack = packReactionRows(self.sizes(3), width: 110.0, topInset: self.topInset)
// Two pills on the first row, one on the second.
XCTAssertEqual(pack.size.width, self.pill.width * 2.0 + reactionButtonSpacing, accuracy: 0.001)
}
/// `lastRowWidth` drives the "does the date fit beside the final row" decision, so it must
/// describe the FINAL row only — not the whole run.
func test_lastRowWidth_describesTheFinalRowOnly() {
let wide = packReactionRows(self.sizes(2), width: 400.0, topInset: self.topInset)
XCTAssertEqual(wide.lastRowWidth, self.pill.width * 2.0 + reactionButtonSpacing, accuracy: 0.001,
"both pills share the only row")
let narrow = packReactionRows(self.sizes(3), width: 110.0, topInset: self.topInset)
XCTAssertEqual(narrow.lastRowWidth, self.pill.width, accuracy: 0.001,
"the trailing row holds one pill")
}
/// The regression itself, stated as the caller sees it: the same reactions must occupy ONE row
/// once there is room for them. Measuring against a narrow width and drawing against a wide one
/// is what produced the extra reserved row; both now come from a pack at the real width.
func test_widthGrowth_collapsesTheReactionsBackOntoOneRow() {
let narrow = packReactionRows(self.sizes(2), width: 60.0, topInset: self.topInset)
XCTAssertEqual(narrow.size.height, self.pill.height * 2.0 + reactionButtonSpacing, accuracy: 0.001,
"two rows when the pills genuinely do not fit")
let wide = packReactionRows(self.sizes(2), width: 200.0, topInset: self.topInset)
XCTAssertEqual(wide.size.height, self.pill.height, accuracy: 0.001,
"one row — and so one row's worth of reserved height — once they fit")
XCTAssertEqual(wide.positions.map { $0.y }, [self.topInset, self.topInset])
}
/// A button wider than the available width used to break the row on the very first iteration,
/// which counted its height once for the flush and again for the trailing row: two rows reserved
/// for one button, with one drawn. A break before anything is placed is not a row.
func test_singleButtonWiderThanTheWidth_isStillOneRow() {
let oversized = CGSize(width: 100.0, height: 30.0)
let pack = packReactionRows([oversized], width: 60.0, topInset: self.topInset)
XCTAssertEqual(pack.positions, [CGPoint(x: reactionButtonLeadingBleed, y: self.topInset)])
XCTAssertEqual(pack.size.height, oversized.height, accuracy: 0.001)
}
func test_noButtons_reservesNothing() {
let pack = packReactionRows([], width: 200.0, topInset: self.topInset)
XCTAssertTrue(pack.positions.isEmpty)
XCTAssertEqual(pack.size.height, 0.0)
XCTAssertEqual(pack.lastRowWidth, 0.0)
}
}