mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 06:14:14 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.0 KiB
Swift
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")
|
|
}
|
|
}
|
|
}
|