mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-04 22:52:45 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
6.4 KiB
Swift
135 lines
6.4 KiB
Swift
import XCTest
|
|
import Postbox
|
|
import TelegramCore
|
|
import RichTextEditorCore
|
|
import RichTextEditorMessageConversion
|
|
@testable import ChatRichTextEditorComposer
|
|
|
|
/// The editor `Document` <-> `ChatInputContent` bridge, for buttons. The composer is the host that
|
|
/// must PRESERVE an incoming button (it has no authoring UI), so these are the round-trips that keep
|
|
/// a button-bearing message intact across an edit and a draft save.
|
|
final class DocumentButtonBridgeTests: XCTestCase {
|
|
private func toContent(_ document: Document) -> ChatInputContent {
|
|
chatInputContent(fromDocument: document, resolveEmoji: { _ in nil }, resolveMedia: { _ in nil })
|
|
}
|
|
|
|
private func toDocument(_ content: ChatInputContent) -> Document {
|
|
document(fromChatInputContent: content,
|
|
registerEmoji: { _, _ in EmojiRef(id: "0", instanceID: "0") },
|
|
registerMedia: { _ in "" })
|
|
}
|
|
|
|
private func row(_ alignment: ButtonRowAlignment = .center) -> ButtonRowBlock {
|
|
ButtonRowBlock(
|
|
id: BlockID.generate(),
|
|
buttons: [ButtonRef(label: [TextRun(text: "Open")], action: .url("https://telegram.org"), color: .primary),
|
|
ButtonRef(label: [TextRun(text: "Nope")], action: .disabled)],
|
|
alignment: alignment
|
|
)
|
|
}
|
|
|
|
func testButtonRowRoundTripsBothDirections() {
|
|
let source = row()
|
|
let content = toContent(Document(blocks: [.buttonRow(source)]))
|
|
guard case let .buttonRow(chatRow) = content.blocks.first else {
|
|
return XCTFail("expected a .buttonRow block, got \(String(describing: content.blocks.first))")
|
|
}
|
|
XCTAssertEqual(chatRow.alignment, .center)
|
|
XCTAssertEqual(chatRow.buttons.count, 2)
|
|
|
|
guard case let .buttonRow(restored) = toDocument(content).blocks.first else {
|
|
return XCTFail("expected a .buttonRow block back")
|
|
}
|
|
XCTAssertEqual(restored.alignment, source.alignment)
|
|
// The BlockID is regenerated by design; the buttons must survive untouched.
|
|
XCTAssertEqual(restored.buttons, source.buttons)
|
|
}
|
|
|
|
/// A preserved non-URL action must survive the composer round-trip verbatim — that is the whole
|
|
/// point of the opaque blob.
|
|
func testPreservedCallbackActionSurvivesTheRoundTrip() {
|
|
let source = ButtonRowBlock(
|
|
id: BlockID.generate(),
|
|
buttons: [ButtonRef(
|
|
label: [TextRun(text: "Tap")],
|
|
action: buttonAction(from: .callback(requiresPassword: true, data: MemoryBuffer(data: Data([7, 8, 9])))),
|
|
color: .danger,
|
|
isLink: true
|
|
)],
|
|
alignment: .right
|
|
)
|
|
guard case let .buttonRow(restored) = toDocument(toContent(Document(blocks: [.buttonRow(source)]))).blocks.first else {
|
|
return XCTFail("expected a .buttonRow block back")
|
|
}
|
|
XCTAssertEqual(restored.buttons, source.buttons)
|
|
}
|
|
|
|
func testInlineButtonRoundTripsAsSingleObjectReplacement() {
|
|
var attributes = CharacterAttributes.plain
|
|
attributes.button = ButtonRef(label: [TextRun(text: "Go")], action: .url("https://telegram.org"), isLink: true)
|
|
let document = Document(blocks: [.paragraph(ParagraphBlock(id: BlockID.generate(), style: .body, runs: [
|
|
TextRun(text: "before "),
|
|
TextRun(text: "\u{FFFC}", attributes: attributes),
|
|
]))])
|
|
|
|
let content = toContent(document)
|
|
guard case let .paragraph(p) = content.blocks.first, p.runs.count == 2 else {
|
|
return XCTFail("expected one paragraph with two runs")
|
|
}
|
|
XCTAssertEqual(p.runs[1].text, "\u{FFFC}")
|
|
guard case .button = p.runs[1].attributes.entity else {
|
|
return XCTFail("expected a .button entity")
|
|
}
|
|
|
|
guard case let .paragraph(bp) = toDocument(content).blocks.first, bp.runs.count == 2 else {
|
|
return XCTFail("expected one paragraph with two runs back")
|
|
}
|
|
XCTAssertEqual(bp.runs[1].text, "\u{FFFC}")
|
|
XCTAssertEqual(bp.runs[1].attributes.button, attributes.button)
|
|
}
|
|
|
|
/// A button row is not representable inside a table cell (a cell is a flat run list). It must
|
|
/// contribute nothing rather than leaking a placeholder or its label into the cell text.
|
|
func testButtonRowInsideTableCellContributesNoRuns() {
|
|
let cell = Cell(id: BlockID.generate(), blocks: [.buttonRow(row())])
|
|
let table = TableBlock(id: BlockID.generate(),
|
|
columns: [ColumnSpec(width: 0)],
|
|
rows: [Row(id: BlockID.generate(), height: nil, cells: [cell])],
|
|
compact: false)
|
|
guard case let .table(t) = toContent(Document(blocks: [.table(table)])).blocks.first else {
|
|
return XCTFail("expected a table")
|
|
}
|
|
XCTAssertTrue(t.rows[0].cells[0].runs.isEmpty)
|
|
}
|
|
|
|
/// A pasted button row cannot be rendered by the legacy input, so it must latch the composer to
|
|
/// the native editor — same rule as a table or a medium.
|
|
func testButtonRowRequiresNativeRichInput() {
|
|
// Exercised through the same predicate `rtfRequiresNativeRichInput` uses on its parsed document.
|
|
XCTAssertTrue(documentRequiresNativeRichInput(Document(blocks: [.buttonRow(row())])))
|
|
XCTAssertFalse(documentRequiresNativeRichInput(Document(blocks: [
|
|
.paragraph(ParagraphBlock(id: BlockID.generate(), style: .body, runs: [TextRun(text: "plain")])),
|
|
])))
|
|
}
|
|
|
|
/// A table's `bordered` flag survives both bridge directions, so a toggle reaches drafts and the wire.
|
|
func testTableBorderedRoundTripsBothDirections() {
|
|
let table = TableBlock(id: BlockID.generate(),
|
|
columns: [ColumnSpec(width: 0)],
|
|
rows: [Row(id: BlockID.generate(), height: nil,
|
|
cells: [Cell(id: BlockID.generate(), blocks: [])])],
|
|
bordered: false)
|
|
let content = toContent(Document(blocks: [.table(table)]))
|
|
guard case let .table(chatTable) = content.blocks.first else {
|
|
return XCTFail("expected a table in the currency")
|
|
}
|
|
XCTAssertFalse(chatTable.bordered)
|
|
|
|
guard case let .table(restored) = toDocument(content).blocks.first else {
|
|
return XCTFail("expected a table back")
|
|
}
|
|
XCTAssertFalse(restored.bordered)
|
|
}
|
|
}
|
|
|