mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
8.9 KiB
Swift
181 lines
8.9 KiB
Swift
import XCTest
|
|
import Postbox
|
|
import TelegramCore
|
|
import RichTextEditorCore
|
|
@testable import RichTextEditorMessageConversion
|
|
|
|
final class ButtonActionCodecTests: XCTestCase {
|
|
private func assertRoundTrips(_ action: ReplyMarkupButtonAction, file: StaticString = #filePath, line: UInt = #line) {
|
|
let core = buttonAction(from: action)
|
|
XCTAssertEqual(replyMarkupButtonAction(from: core), action, file: file, line: line)
|
|
}
|
|
|
|
func testNamedActionsRoundTrip() {
|
|
assertRoundTrips(.url("https://telegram.org"))
|
|
assertRoundTrips(.copyText(payload: "hello"))
|
|
assertRoundTrips(.disabled)
|
|
}
|
|
|
|
func testNamedActionsUseNamedCases() {
|
|
XCTAssertEqual(buttonAction(from: .url("https://telegram.org")), .url("https://telegram.org"))
|
|
XCTAssertEqual(buttonAction(from: .copyText(payload: "hi")), .copyText("hi"))
|
|
XCTAssertEqual(buttonAction(from: .disabled), .disabled)
|
|
}
|
|
|
|
/// Every action Core cannot name must survive verbatim through the opaque blob.
|
|
func testUnsupportedActionsRoundTripVerbatim() {
|
|
assertRoundTrips(.callback(requiresPassword: true, data: MemoryBuffer(data: Data([0x01, 0x02, 0xff]))))
|
|
assertRoundTrips(.urlAuth(url: "https://telegram.org", buttonId: 7))
|
|
assertRoundTrips(.openWebView(url: "https://telegram.org/app", simple: true))
|
|
assertRoundTrips(.switchInline(samePeer: true, query: "q", peerTypes: [.users, .bots]))
|
|
assertRoundTrips(.openWebApp)
|
|
assertRoundTrips(.payment)
|
|
}
|
|
|
|
func testUnsupportedActionIsOpaqueToCore() {
|
|
guard case let .unsupported(kind, payload) = buttonAction(from: .payment) else {
|
|
return XCTFail("expected .unsupported")
|
|
}
|
|
XCTAssertFalse(kind.isEmpty)
|
|
XCTAssertFalse(payload.isEmpty)
|
|
}
|
|
|
|
/// A blob written by a future build with an unknown `kind`, or a corrupt payload, must degrade
|
|
/// rather than crash — a Document can arrive from a newer build via .rtdoc or the clipboard.
|
|
func testUnknownBlobDegradesToDisabled() {
|
|
XCTAssertEqual(replyMarkupButtonAction(from: .unsupported(kind: "notARealKind", payload: "%%%")), .disabled)
|
|
XCTAssertEqual(replyMarkupButtonAction(from: .unsupported(kind: "notARealKind", payload: "")), .disabled)
|
|
}
|
|
|
|
func testColorAndAlignmentMapBothWays() {
|
|
XCTAssertEqual(buttonColor(from: .danger), .danger)
|
|
XCTAssertNil(buttonColor(from: nil))
|
|
XCTAssertEqual(replyMarkupColor(from: .success), .success)
|
|
XCTAssertNil(replyMarkupColor(from: nil))
|
|
XCTAssertEqual(buttonRowAlignment(from: .center), .center)
|
|
XCTAssertEqual(instantPageRowAlignment(from: .justify), .justify)
|
|
}
|
|
|
|
/// The raw values are persisted on both sides and must stay in lockstep.
|
|
func testColorAndAlignmentRawValuesAgreeAcrossTheSeam() {
|
|
XCTAssertEqual(ButtonColor.primary.rawValue, ReplyMarkupButton.Style.Color.primary.rawValue)
|
|
XCTAssertEqual(ButtonColor.danger.rawValue, ReplyMarkupButton.Style.Color.danger.rawValue)
|
|
XCTAssertEqual(ButtonColor.success.rawValue, ReplyMarkupButton.Style.Color.success.rawValue)
|
|
XCTAssertEqual(ButtonRowAlignment.justify.rawValue, InstantPageButtonRowAlignment.justify.rawValue)
|
|
XCTAssertEqual(ButtonRowAlignment.left.rawValue, InstantPageButtonRowAlignment.left.rawValue)
|
|
XCTAssertEqual(ButtonRowAlignment.center.rawValue, InstantPageButtonRowAlignment.center.rawValue)
|
|
XCTAssertEqual(ButtonRowAlignment.right.rawValue, InstantPageButtonRowAlignment.right.rawValue)
|
|
}
|
|
|
|
func testChatInputButtonRoundTripsThroughButtonRef() {
|
|
let original = ChatInputButton(
|
|
label: [ChatInputRun(text: "Open")],
|
|
action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data([9]))),
|
|
color: .danger,
|
|
isLink: true
|
|
)
|
|
XCTAssertEqual(chatInputButton(fromButtonRef: buttonRef(fromChatInputButton: original)), original)
|
|
}
|
|
|
|
/// Inline formatting on a label survives; a `.url` entity becomes the run's `link`.
|
|
func testLabelFormattingRoundTrips() {
|
|
let original = ChatInputButton(
|
|
label: [
|
|
ChatInputRun(text: "bold", attributes: ChatInputInlineAttributes(bold: true)),
|
|
ChatInputRun(text: "link", attributes: {
|
|
var a = ChatInputInlineAttributes()
|
|
a.entity = .url("https://telegram.org")
|
|
return a
|
|
}()),
|
|
],
|
|
action: .url("https://telegram.org")
|
|
)
|
|
XCTAssertEqual(chatInputButton(fromButtonRef: buttonRef(fromChatInputButton: original)), original)
|
|
}
|
|
}
|
|
|
|
/// The article-editor send path (`composeRichMessage`). A button has no message-entity form, so its
|
|
/// presence must force the rich InstantPage path — otherwise the message is sent as plain text +
|
|
/// entities and the buttons are destroyed on the way out.
|
|
final class ButtonSendPathTests: XCTestCase {
|
|
private func paragraph(_ runs: [TextRun]) -> Block {
|
|
.paragraph(ParagraphBlock(id: BlockID.generate(), style: .body, runs: runs))
|
|
}
|
|
|
|
private func inlineButtonRun(_ label: String) -> TextRun {
|
|
var attributes = CharacterAttributes.plain
|
|
attributes.button = ButtonRef(label: [TextRun(text: label)], action: .url("https://telegram.org"))
|
|
return TextRun(text: "\u{FFFC}", attributes: attributes)
|
|
}
|
|
|
|
func testButtonRowForcesTheRichPath() {
|
|
let row = ButtonRowBlock(id: BlockID.generate(),
|
|
buttons: [ButtonRef(label: [TextRun(text: "Go")], action: .url("https://telegram.org"))],
|
|
alignment: .justify)
|
|
let document = Document(blocks: [paragraph([TextRun(text: "hi")]), .buttonRow(row)])
|
|
guard case let .rich(page) = composeRichMessage(from: document) else {
|
|
return XCTFail("a button row must force the rich path")
|
|
}
|
|
XCTAssertTrue(page.blocks.contains { if case .buttonRow = $0 { return true } else { return false } })
|
|
}
|
|
|
|
func testInlineButtonForcesTheRichPath() {
|
|
let document = Document(blocks: [paragraph([TextRun(text: "tap "), inlineButtonRun("Go")])])
|
|
guard case .rich = composeRichMessage(from: document) else {
|
|
return XCTFail("an inline button must force the rich path")
|
|
}
|
|
}
|
|
|
|
/// The pill must reach the page as a `RichText.textButton`. It is an if-let chain rather than an
|
|
/// exhaustive switch, so a missing arm compiles and silently emits the bare U+FFFC placeholder.
|
|
func testInlineButtonEmitsATextButtonNotAPlaceholder() {
|
|
let document = Document(blocks: [paragraph([inlineButtonRun("Go")])])
|
|
guard case let .rich(page) = composeRichMessage(from: document),
|
|
case let .paragraph(text) = page.blocks.first else {
|
|
return XCTFail("expected one rich paragraph block")
|
|
}
|
|
guard case let .textButton(button) = text else {
|
|
return XCTFail("expected a RichText.textButton, got \(text)")
|
|
}
|
|
XCTAssertEqual(button.text.plainText, "Go")
|
|
XCTAssertEqual(button.action, .url("https://telegram.org"))
|
|
}
|
|
|
|
func testEmittedRowCarriesAlignmentActionAndStyle() {
|
|
let row = ButtonRowBlock(
|
|
id: BlockID.generate(),
|
|
buttons: [ButtonRef(label: [TextRun(text: "Open")], action: .url("https://telegram.org"), color: .success, isLink: true)],
|
|
alignment: .center
|
|
)
|
|
guard case let .rich(page) = composeRichMessage(from: Document(blocks: [.buttonRow(row)])),
|
|
case let .buttonRow(alignment, buttons) = page.blocks.first else {
|
|
return XCTFail("expected one buttonRow block")
|
|
}
|
|
XCTAssertEqual(alignment, .center)
|
|
XCTAssertEqual(buttons[0].action, .url("https://telegram.org"))
|
|
XCTAssertEqual(buttons[0].color, .success)
|
|
XCTAssertTrue(buttons[0].isLink)
|
|
}
|
|
|
|
/// A preserved non-URL action must reach the wire intact.
|
|
func testPreservedActionReachesThePage() {
|
|
let action = ReplyMarkupButtonAction.callback(requiresPassword: false, data: MemoryBuffer(data: Data([4, 2])))
|
|
let row = ButtonRowBlock(id: BlockID.generate(),
|
|
buttons: [ButtonRef(label: [TextRun(text: "Cb")], action: buttonAction(from: action))],
|
|
alignment: .justify)
|
|
guard case let .rich(page) = composeRichMessage(from: Document(blocks: [.buttonRow(row)])),
|
|
case let .buttonRow(_, buttons) = page.blocks.first else {
|
|
return XCTFail("expected one buttonRow block")
|
|
}
|
|
XCTAssertEqual(buttons[0].action, action)
|
|
}
|
|
|
|
/// Plain content must still take the entity path — the gate must not over-trigger.
|
|
func testPlainContentStillTakesTheEntityPath() {
|
|
guard case .plain = composeRichMessage(from: Document(blocks: [paragraph([TextRun(text: "just text")])])) else {
|
|
return XCTFail("plain text must not be promoted to the rich path")
|
|
}
|
|
}
|
|
}
|
|
|