Files
ghostgram/submodules/TextFormat/Tests/InstantPageBlockQuoteCollapsedWireTests.swift
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

100 lines
5.0 KiB
Swift

import XCTest
import TelegramApi
@testable import TelegramCore
/// What survives a send: `blockQuote.collapsed` across `apiInputBlock()` → the wire → `init(apiBlock:)`.
///
/// This matters because a rich message's own page does NOT survive the send — the pending message
/// renders the client's `InstantPage`, and the confirmed one is rebuilt from what the server echoes
/// back (`RichTextMessageAttribute(apiRichMessage:)` in `StoreMessage_Telegram`). Anything the wire
/// drops therefore reappears as "it looked right until I sent it".
final class InstantPageBlockQuoteCollapsedWireTests: XCTestCase {
/// `block` encoded to its API constructor, serialised, parsed back and decoded — the exact path a
/// quote takes between the composer and the confirmed message.
private func wireRoundTrip(_ block: InstantPageBlock) -> InstantPageBlock? {
guard let apiBlock = block.apiInputBlock() else {
return nil
}
let buffer = Buffer()
// Unboxed: `Api.parse(_:signature:)` is internal to TelegramApi, so the per-constructor
// parsers are called directly and must not be handed a leading constructor id.
apiBlock.serialize(buffer, false)
let reader = BufferReader(buffer)
let parsed: Api.PageBlock?
switch apiBlock {
case .pageBlockBlockquote:
parsed = Api.PageBlock.parse_pageBlockBlockquote(reader)
case .pageBlockBlockquoteBlocks:
parsed = Api.PageBlock.parse_pageBlockBlockquoteBlocks(reader)
default:
parsed = nil
}
return parsed.map { InstantPageBlock(apiBlock: $0) }
}
private func collapsedState(_ block: InstantPageBlock?) -> Bool? {
guard case let .blockQuote(_, _, collapsed) = block else {
return nil
}
return collapsed
}
private func quote(paragraphs: [String], collapsed: Bool) -> InstantPageBlock {
return .blockQuote(
blocks: paragraphs.map { .paragraph(.plain($0)) },
caption: .empty,
collapsed: collapsed)
}
// MARK: The one shape that survives
/// A quote of exactly ONE paragraph encodes as `pageBlockBlockquote`, which carries `flags.0`.
func test_singleParagraphQuoteKeepsCollapsedAcrossTheWire() {
let sent = self.quote(paragraphs: ["a long enough quote to be worth collapsing"], collapsed: true)
XCTAssertEqual(self.collapsedState(self.wireRoundTrip(sent)), true)
}
func test_singleParagraphQuoteKeepsNotCollapsedAcrossTheWire() {
let sent = self.quote(paragraphs: ["one line"], collapsed: false)
XCTAssertEqual(self.collapsedState(self.wireRoundTrip(sent)), false)
}
// MARK: The gap
/// **Everything else loses the flag.** `apiInputBlock` can only reach for
/// `pageBlockBlockquoteBlocks` once a quote holds more than a lone paragraph, and that
/// constructor has no `flags` field at all — so a collapsed quote the reader can actually
/// collapse (four lines and up is usually four Returns, not one wrapped sentence) arrives
/// expanded and un-collapsible.
///
/// Pinned as a test rather than left as a comment because the symptom — "it was collapsed in the
/// composer and is not collapsed in the chat" — reads as a rendering bug and sends you looking in
/// the wrong module. Closing it needs `pageBlockBlockquoteBlocks flags:# collapsed:flags.0?true`,
/// which is a coordinated schema change; when that lands, this assertion is the one that fails.
func test_multiParagraphQuoteLosesCollapsedAcrossTheWire() {
let sent = self.quote(paragraphs: ["first line", "second line", "third line", "fourth line"], collapsed: true)
XCTAssertEqual(self.collapsedState(self.wireRoundTrip(sent)), false, "known gap: pageBlockBlockquoteBlocks has no flags")
}
/// Not about paragraph COUNT — about the shape. A quote holding one heading takes the same
/// flagless constructor as a four-paragraph one.
func test_singleNonParagraphQuoteAlsoLosesCollapsed() {
let sent = InstantPageBlock.blockQuote(
blocks: [.heading(text: .plain("a heading"), level: 2)],
caption: .empty,
collapsed: true)
XCTAssertEqual(self.collapsedState(self.wireRoundTrip(sent)), false)
}
/// The encoder's fork, asserted directly, so a future edit that changes WHICH constructor is
/// chosen shows up here rather than only as a lost flag three tests down.
func test_encoderPicksTheFlaglessConstructorForAnythingButALoneParagraph() {
guard case .pageBlockBlockquote = self.quote(paragraphs: ["only"], collapsed: true).apiInputBlock() else {
return XCTFail("a lone paragraph must take the constructor that has flags")
}
guard case .pageBlockBlockquoteBlocks = self.quote(paragraphs: ["one", "two"], collapsed: true).apiInputBlock() else {
return XCTFail("more than one block must take pageBlockBlockquoteBlocks")
}
}
}