mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
8.4 KiB
Swift
200 lines
8.4 KiB
Swift
import XCTest
|
|
import UIKit
|
|
import TelegramCore
|
|
@testable import TextFormat
|
|
|
|
private let testFont = UIFont.systemFont(ofSize: 17.0)
|
|
|
|
private func customEmojiAttribute(_ string: NSAttributedString, at index: Int) -> (fileId: Int64, range: NSRange)? {
|
|
var effectiveRange = NSRange(location: 0, length: 0)
|
|
guard let value = string.attribute(ChatTextInputAttributes.customEmoji, at: index, effectiveRange: &effectiveRange) as? ChatTextInputTextCustomEmojiAttribute else {
|
|
return nil
|
|
}
|
|
return (value.fileId, effectiveRange)
|
|
}
|
|
|
|
private func hasCustomEmojiAttribute(_ string: NSAttributedString) -> Bool {
|
|
var found = false
|
|
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: string.length), options: [], using: { value, _, _ in
|
|
if value != nil {
|
|
found = true
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
private func applyEntities(_ text: String, _ entities: [MessageTextEntity]) -> NSAttributedString {
|
|
return stringWithAppliedEntities(
|
|
text,
|
|
entities: entities,
|
|
baseColor: .black,
|
|
linkColor: .blue,
|
|
baseFont: testFont,
|
|
linkFont: testFont,
|
|
boldFont: testFont,
|
|
italicFont: testFont,
|
|
boldItalicFont: testFont,
|
|
fixedFont: testFont,
|
|
blockQuoteFont: testFont,
|
|
message: nil
|
|
)
|
|
}
|
|
|
|
final class StringWithAppliedCustomEmojiEntitiesTests: XCTestCase {
|
|
func testAttributeAppliedToEmojiAtEndOfText() {
|
|
// A message that *ends* with a custom emoji is the common case, and the emoji's range ends
|
|
// exactly at the end of the string.
|
|
let text = "hi 😀"
|
|
XCTAssertEqual((text as NSString).length, 5)
|
|
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: text), entities: [
|
|
MessageTextEntity(range: 3 ..< 5, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
], message: nil)
|
|
|
|
let applied = customEmojiAttribute(result, at: 3)
|
|
XCTAssertEqual(applied?.fileId, 42)
|
|
XCTAssertEqual(applied?.range, NSRange(location: 3, length: 2))
|
|
}
|
|
|
|
func testAttributeAppliedToEmojiInsideText() {
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: "😀 hi"), entities: [
|
|
MessageTextEntity(range: 0 ..< 2, type: .CustomEmoji(stickerPack: nil, fileId: 7))
|
|
], message: nil)
|
|
|
|
let applied = customEmojiAttribute(result, at: 0)
|
|
XCTAssertEqual(applied?.fileId, 7)
|
|
XCTAssertEqual(applied?.range, NSRange(location: 0, length: 2))
|
|
}
|
|
|
|
func testEntityStartingBeyondTextIsIgnored() {
|
|
// Entity offsets are not trustworthy: they can describe a longer text than the one being
|
|
// rendered (a live typing draft delivers text and entities as separate streamed values).
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: "hi"), entities: [
|
|
MessageTextEntity(range: 5 ..< 7, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
], message: nil)
|
|
|
|
XCTAssertEqual(result.string, "hi")
|
|
XCTAssertFalse(hasCustomEmojiAttribute(result))
|
|
}
|
|
|
|
func testEntityExtendingBeyondTextIsIgnored() {
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: "hi 😀"), entities: [
|
|
MessageTextEntity(range: 3 ..< 9, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
], message: nil)
|
|
|
|
XCTAssertEqual(result.string, "hi 😀")
|
|
XCTAssertFalse(hasCustomEmojiAttribute(result))
|
|
}
|
|
|
|
func testEmptyTextWithEntityIsIgnored() {
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: ""), entities: [
|
|
MessageTextEntity(range: 0 ..< 2, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
], message: nil)
|
|
|
|
XCTAssertEqual(result.length, 0)
|
|
}
|
|
|
|
func testNonCustomEmojiEntitiesAreIgnored() {
|
|
let result = stringWithAppliedCustomEmojiEntities(NSAttributedString(string: "hi 😀"), entities: [
|
|
MessageTextEntity(range: 0 ..< 2, type: .Bold)
|
|
], message: nil)
|
|
|
|
XCTAssertFalse(hasCustomEmojiAttribute(result))
|
|
}
|
|
}
|
|
|
|
final class AdjustedEntityRangesTests: XCTestCase {
|
|
private func adjust(_ text: String, _ entities: [MessageTextEntity], replacement: String) -> (string: NSMutableAttributedString, ranges: [NSRange?]) {
|
|
let string = NSMutableAttributedString(string: text)
|
|
let ranges = adjustedEntityRangesApplyingSubstitutions(string, entities: entities, baseAttributes: [:], formattedDateReplacement: { _, _ in
|
|
return replacement
|
|
})
|
|
return (string, ranges)
|
|
}
|
|
|
|
func testRangesFollowSubstitutionWhenEntitiesAreSorted() {
|
|
let text = "2026-01-01 12:00 hi 😀"
|
|
let (string, ranges) = adjust(text, [
|
|
MessageTextEntity(range: 0 ..< 16, type: .FormattedDate(format: .relative, date: 1767268800)),
|
|
MessageTextEntity(range: 20 ..< 22, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
], replacement: "now")
|
|
|
|
XCTAssertEqual(string.string, "now hi 😀")
|
|
XCTAssertEqual(ranges[0], NSRange(location: 0, length: 3))
|
|
XCTAssertEqual(ranges[1], NSRange(location: 7, length: 2))
|
|
XCTAssertEqual((string.string as NSString).substring(with: ranges[1]!), "😀")
|
|
}
|
|
|
|
func testRangesFollowSubstitutionWhenEntitiesAreUnsorted() {
|
|
// `addLocallyGeneratedEntities` appends client-detected entities after the server's, so the
|
|
// array order does not follow the text order. The running substitution delta is only
|
|
// correct when the entities are visited in text order.
|
|
let text = "2026-01-01 12:00 hi 😀"
|
|
let (string, ranges) = adjust(text, [
|
|
MessageTextEntity(range: 20 ..< 22, type: .CustomEmoji(stickerPack: nil, fileId: 42)),
|
|
MessageTextEntity(range: 0 ..< 16, type: .FormattedDate(format: .relative, date: 1767268800))
|
|
], replacement: "now")
|
|
|
|
XCTAssertEqual(string.string, "now hi 😀")
|
|
XCTAssertEqual(ranges[1], NSRange(location: 0, length: 3))
|
|
XCTAssertEqual(ranges[0], NSRange(location: 7, length: 2))
|
|
XCTAssertEqual((string.string as NSString).substring(with: ranges[0]!), "😀")
|
|
}
|
|
|
|
func testEntitySwallowedBySubstitutionHasNoRange() {
|
|
// The bold entity sits inside the text the substitution replaces. Shifting it by the delta
|
|
// would put it before the start of the string; there is nothing left for it to point at.
|
|
let text = "2026-01-01 12:00 hi"
|
|
let (string, ranges) = adjust(text, [
|
|
MessageTextEntity(range: 0 ..< 16, type: .FormattedDate(format: .relative, date: 1767268800)),
|
|
MessageTextEntity(range: 5 ..< 7, type: .Bold)
|
|
], replacement: "now")
|
|
|
|
XCTAssertEqual(string.string, "now hi")
|
|
XCTAssertEqual(ranges[0], NSRange(location: 0, length: 3))
|
|
XCTAssertNil(ranges[1])
|
|
}
|
|
|
|
func testEntityBeyondTextHasNoRange() {
|
|
let (_, ranges) = adjust("hi", [
|
|
MessageTextEntity(range: 5 ..< 7, type: .Bold)
|
|
], replacement: "now")
|
|
|
|
XCTAssertNil(ranges[0])
|
|
}
|
|
}
|
|
|
|
final class StringWithAppliedEntitiesCustomEmojiTests: XCTestCase {
|
|
// `stringWithAppliedEntities` applies the custom-emoji attribute itself. Callers must not
|
|
// re-apply it from the entities' own offsets: those describe the source text, which the
|
|
// substitution pass above is free to reshape.
|
|
func testCustomEmojiAttributeIsApplied() {
|
|
let result = applyEntities("hi 😀", [
|
|
MessageTextEntity(range: 3 ..< 5, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
])
|
|
|
|
let applied = customEmojiAttribute(result, at: 3)
|
|
XCTAssertEqual(applied?.fileId, 42)
|
|
XCTAssertEqual(applied?.range, NSRange(location: 3, length: 2))
|
|
}
|
|
|
|
func testCustomEmojiEntityBeyondTextIsIgnored() {
|
|
let result = applyEntities("hi", [
|
|
MessageTextEntity(range: 5 ..< 7, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
])
|
|
|
|
XCTAssertEqual(result.string, "hi")
|
|
XCTAssertFalse(hasCustomEmojiAttribute(result))
|
|
}
|
|
|
|
func testCustomEmojiEntityExtendingBeyondTextIsClamped() {
|
|
let result = applyEntities("hi 😀", [
|
|
MessageTextEntity(range: 3 ..< 9, type: .CustomEmoji(stickerPack: nil, fileId: 42))
|
|
])
|
|
|
|
let applied = customEmojiAttribute(result, at: 3)
|
|
XCTAssertEqual(applied?.fileId, 42)
|
|
XCTAssertEqual(applied?.range, NSRange(location: 3, length: 2))
|
|
}
|
|
}
|