450 lines
19 KiB
Swift
450 lines
19 KiB
Swift
import XCTest
|
|
@testable import Rosetta
|
|
|
|
// MARK: - Reply Preview Text Tests (ChatDetailView.replyPreviewText parity)
|
|
|
|
/// Tests for reply preview text logic — cross-platform parity with Android/Desktop.
|
|
/// Android: `ChatDetailInput.kt` lines 653-679
|
|
/// Desktop: `constructLastMessageTextByAttachments.ts`
|
|
///
|
|
/// Covers ALL 3 iOS display contexts:
|
|
/// 1. Reply bar input — `ChatDetailView.replyPreviewText(for:)`
|
|
/// 2. Reply bar render — `ChatDetailView.replyBar(for:)` inline closure
|
|
/// 3. Reply quote bubble — `MessageCellView.replyQuoteView(reply:outgoing:)`
|
|
/// Plus: chat list last message — `DialogRepository.updateDialogFromMessages()`
|
|
final class ReplyPreviewTextTests: XCTestCase {
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// Standalone replica of `ChatDetailView.replyPreviewText(for:)`.
|
|
/// Must match the real implementation exactly.
|
|
private func replyPreviewText(for message: ChatMessage) -> String {
|
|
if message.attachments.contains(where: { $0.type == .image }) {
|
|
let caption = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return caption.isEmpty ? "Photo" : caption
|
|
}
|
|
if let file = message.attachments.first(where: { $0.type == .file }) {
|
|
let caption = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !caption.isEmpty { return caption }
|
|
let parsed = AttachmentPreviewCodec.parseFilePreview(file.preview)
|
|
if !parsed.fileName.isEmpty { return parsed.fileName }
|
|
return file.id.isEmpty ? "File" : file.id
|
|
}
|
|
if message.attachments.contains(where: { $0.type == .avatar }) { return "Avatar" }
|
|
if message.attachments.contains(where: { $0.type == .messages }) { return "Forwarded message" }
|
|
if message.attachments.contains(where: { $0.type == .call }) { return "Call" }
|
|
let trimmed = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !trimmed.isEmpty { return message.text }
|
|
if !message.attachments.isEmpty { return "Attachment" }
|
|
return ""
|
|
}
|
|
|
|
/// Standalone replica of `MessageCellView.replyQuoteView` preview logic.
|
|
/// Uses `ReplyMessageData` + `ReplyAttachmentData` (Int type), not `MessageAttachment`.
|
|
private func replyQuotePreviewText(for reply: ReplyMessageData) -> String {
|
|
let trimmed = reply.message.trimmingCharacters(in: .whitespaces)
|
|
if !trimmed.isEmpty { return reply.message }
|
|
if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
|
|
if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
|
|
if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
|
|
if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
|
|
if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
|
|
return "Attachment"
|
|
}
|
|
|
|
/// Standalone replica of `DialogRepository.updateDialogFromMessages` preview logic.
|
|
private func chatListPreviewText(text: String, attachments: [MessageAttachment]) -> String {
|
|
let textIsEmpty = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
|| Self.isGarbageText(text)
|
|
if textIsEmpty, let firstAtt = attachments.first {
|
|
switch firstAtt.type {
|
|
case .image: return "Photo"
|
|
case .file: return "File"
|
|
case .avatar: return "Avatar"
|
|
case .messages: return "Forwarded message"
|
|
case .call: return "Call"
|
|
}
|
|
} else if textIsEmpty {
|
|
return ""
|
|
}
|
|
return text
|
|
}
|
|
|
|
/// Mirror of `DialogRepository.isGarbageText`.
|
|
private static func isGarbageText(_ text: String) -> Bool {
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.isEmpty { return true }
|
|
|
|
// Detect encrypted payload: ivBase64:ctBase64 format
|
|
let parts = trimmed.components(separatedBy: ":")
|
|
if parts.count == 2 {
|
|
let base64Chars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "+/="))
|
|
let bothBase64 = parts.allSatisfy { part in
|
|
part.count >= 16 && part.unicodeScalars.allSatisfy { base64Chars.contains($0) }
|
|
}
|
|
if bothBase64 { return true }
|
|
}
|
|
if trimmed.hasPrefix("CHNK:") { return true }
|
|
|
|
let validCharacters = trimmed.unicodeScalars.filter { scalar in
|
|
scalar.value != 0xFFFD &&
|
|
scalar.value > 0x1F &&
|
|
scalar.value != 0x7F &&
|
|
!CharacterSet.controlCharacters.contains(scalar)
|
|
}
|
|
return validCharacters.isEmpty
|
|
}
|
|
|
|
private func makeMessage(
|
|
text: String = "",
|
|
attachments: [MessageAttachment] = []
|
|
) -> ChatMessage {
|
|
ChatMessage(
|
|
id: UUID().uuidString,
|
|
fromPublicKey: "sender",
|
|
toPublicKey: "recipient",
|
|
text: text,
|
|
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
|
|
deliveryStatus: .delivered,
|
|
isRead: true,
|
|
attachments: attachments
|
|
)
|
|
}
|
|
|
|
private func makeAttachment(type: AttachmentType, preview: String = "", id: String = "") -> MessageAttachment {
|
|
MessageAttachment(id: id.isEmpty ? UUID().uuidString : id, preview: preview, blob: "", type: type)
|
|
}
|
|
|
|
private func makeReply(
|
|
message: String = "",
|
|
attachments: [ReplyAttachmentData] = []
|
|
) -> ReplyMessageData {
|
|
ReplyMessageData(
|
|
message_id: UUID().uuidString,
|
|
publicKey: "sender",
|
|
message: message,
|
|
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
|
|
attachments: attachments
|
|
)
|
|
}
|
|
|
|
private func makeReplyAttachment(type: Int, preview: String = "") -> ReplyAttachmentData {
|
|
ReplyAttachmentData(id: UUID().uuidString, type: type, preview: preview, blob: "")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - Reply Bar Preview (ChatDetailView.replyPreviewText)
|
|
// =========================================================================
|
|
|
|
// MARK: Photo
|
|
|
|
func testReplyBar_PhotoNoCaption() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .image)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
}
|
|
|
|
func testReplyBar_PhotoWithCaption() {
|
|
let msg = makeMessage(text: "Nice sunset", attachments: [makeAttachment(type: .image)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Nice sunset")
|
|
}
|
|
|
|
func testReplyBar_PhotoWithWhitespaceCaption() {
|
|
let msg = makeMessage(text: " \n ", attachments: [makeAttachment(type: .image)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
}
|
|
|
|
func testReplyBar_MultiplePhotos() {
|
|
let msg = makeMessage(attachments: [
|
|
makeAttachment(type: .image),
|
|
makeAttachment(type: .image),
|
|
makeAttachment(type: .image)
|
|
])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
}
|
|
|
|
// MARK: File
|
|
|
|
func testReplyBar_FileWithFilename() {
|
|
let att = makeAttachment(type: .file, preview: "tag::12345::document.pdf")
|
|
let msg = makeMessage(attachments: [att])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "document.pdf")
|
|
}
|
|
|
|
func testReplyBar_FileNoPreview() {
|
|
let att = makeAttachment(type: .file, id: "file_123")
|
|
let msg = makeMessage(attachments: [att])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "file_123")
|
|
}
|
|
|
|
func testReplyBar_FileEmptyIdAndPreview() {
|
|
let att = MessageAttachment(id: "", preview: "", blob: "", type: .file)
|
|
let msg = makeMessage(attachments: [att])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "File")
|
|
}
|
|
|
|
func testReplyBar_FileWithCaption() {
|
|
let att = makeAttachment(type: .file, preview: "tag::500::notes.txt")
|
|
let msg = makeMessage(text: "Here are the notes", attachments: [att])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Here are the notes")
|
|
}
|
|
|
|
// MARK: Avatar
|
|
|
|
func testReplyBar_Avatar() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .avatar)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Avatar")
|
|
}
|
|
|
|
// MARK: Forward
|
|
|
|
func testReplyBar_Forward() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .messages)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Forwarded message")
|
|
}
|
|
|
|
// MARK: Call (Android/Desktop parity)
|
|
|
|
func testReplyBar_Call() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .call)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Call")
|
|
}
|
|
|
|
// MARK: Text
|
|
|
|
func testReplyBar_TextOnly() {
|
|
let msg = makeMessage(text: "Hello world")
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Hello world")
|
|
}
|
|
|
|
func testReplyBar_Empty() {
|
|
let msg = makeMessage()
|
|
XCTAssertEqual(replyPreviewText(for: msg), "")
|
|
}
|
|
|
|
// MARK: Priority
|
|
|
|
func testReplyBar_ImagePriorityOverCall() {
|
|
let msg = makeMessage(attachments: [
|
|
makeAttachment(type: .image),
|
|
makeAttachment(type: .call)
|
|
])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
}
|
|
|
|
func testReplyBar_ImagePriorityOverFile() {
|
|
let msg = makeMessage(attachments: [
|
|
makeAttachment(type: .image),
|
|
makeAttachment(type: .file, preview: "tag::100::doc.pdf")
|
|
])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
}
|
|
|
|
func testReplyBar_FilePriorityOverAvatar() {
|
|
let msg = makeMessage(attachments: [
|
|
makeAttachment(type: .file, preview: "tag::100::doc.pdf"),
|
|
makeAttachment(type: .avatar)
|
|
])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "doc.pdf")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - Reply Quote Bubble (MessageCellView.replyQuoteView)
|
|
// =========================================================================
|
|
|
|
func testQuote_PhotoNoText() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 0)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
|
|
}
|
|
|
|
func testQuote_PhotoWithText() {
|
|
let reply = makeReply(message: "Check this out", attachments: [makeReplyAttachment(type: 0)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Check this out")
|
|
}
|
|
|
|
func testQuote_File() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 2)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "File")
|
|
}
|
|
|
|
func testQuote_Avatar() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 3)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Avatar")
|
|
}
|
|
|
|
func testQuote_Forward() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 1)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Forwarded message")
|
|
}
|
|
|
|
func testQuote_Call() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 4)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Call")
|
|
}
|
|
|
|
func testQuote_UnknownType() {
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 99)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Attachment")
|
|
}
|
|
|
|
func testQuote_TextPriority() {
|
|
let reply = makeReply(message: "Some text", attachments: [makeReplyAttachment(type: 4)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Some text")
|
|
}
|
|
|
|
func testQuote_WhitespaceText() {
|
|
let reply = makeReply(message: " ", attachments: [makeReplyAttachment(type: 0)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - Chat List Preview (DialogRepository)
|
|
// =========================================================================
|
|
|
|
func testChatList_Photo() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .image)]), "Photo")
|
|
}
|
|
|
|
func testChatList_File() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .file)]), "File")
|
|
}
|
|
|
|
func testChatList_Avatar() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .avatar)]), "Avatar")
|
|
}
|
|
|
|
func testChatList_Forward() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .messages)]), "Forwarded message")
|
|
}
|
|
|
|
func testChatList_Call() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .call)]), "Call")
|
|
}
|
|
|
|
func testChatList_TextWins() {
|
|
XCTAssertEqual(chatListPreviewText(text: "Hello", attachments: [makeAttachment(type: .image)]), "Hello")
|
|
}
|
|
|
|
func testChatList_EmptyNoAttachment() {
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: []), "")
|
|
}
|
|
|
|
func testChatList_GarbageTextShowsAttachment() {
|
|
// U+FFFD replacement chars = garbage text → should show attachment label
|
|
let garbage = "\u{FFFD}\u{FFFD}\u{FFFD}"
|
|
XCTAssertEqual(chatListPreviewText(text: garbage, attachments: [makeAttachment(type: .image)]), "Photo")
|
|
}
|
|
|
|
func testChatList_ControlCharsShowsAttachment() {
|
|
let control = "\u{01}\u{02}\u{03}"
|
|
XCTAssertEqual(chatListPreviewText(text: control, attachments: [makeAttachment(type: .call)]), "Call")
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - AttachmentType JSON Decode Fault Tolerance
|
|
// =========================================================================
|
|
|
|
func testDecode_UnknownTypeFallsBackToImage() throws {
|
|
let json = #"[{"id":"test","preview":"","blob":"","type":99}]"#
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
|
|
XCTAssertEqual(decoded.count, 1)
|
|
XCTAssertEqual(decoded[0].type, .image)
|
|
}
|
|
|
|
func testDecode_NegativeTypeFallsBackToImage() throws {
|
|
let json = #"[{"id":"test","preview":"","blob":"","type":-1}]"#
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
|
|
XCTAssertEqual(decoded.count, 1)
|
|
XCTAssertEqual(decoded[0].type, .image)
|
|
}
|
|
|
|
func testDecode_MixedKnownAndUnknown() throws {
|
|
let json = #"[{"id":"a","preview":"","blob":"","type":0},{"id":"b","preview":"","blob":"","type":99}]"#
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
|
|
XCTAssertEqual(decoded.count, 2)
|
|
XCTAssertEqual(decoded[0].type, .image)
|
|
XCTAssertEqual(decoded[1].type, .image)
|
|
}
|
|
|
|
func testDecode_AllKnownTypes() throws {
|
|
let json = """
|
|
[{"id":"a","preview":"","blob":"","type":0},\
|
|
{"id":"b","preview":"","blob":"","type":1},\
|
|
{"id":"c","preview":"","blob":"","type":2},\
|
|
{"id":"d","preview":"","blob":"","type":3},\
|
|
{"id":"e","preview":"","blob":"","type":4}]
|
|
"""
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
|
|
XCTAssertEqual(decoded.count, 5)
|
|
XCTAssertEqual(decoded[0].type, .image)
|
|
XCTAssertEqual(decoded[1].type, .messages)
|
|
XCTAssertEqual(decoded[2].type, .file)
|
|
XCTAssertEqual(decoded[3].type, .avatar)
|
|
XCTAssertEqual(decoded[4].type, .call)
|
|
}
|
|
|
|
func testDecode_EmptyArrayIsValid() throws {
|
|
let json = "[]"
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
|
|
XCTAssertEqual(decoded.count, 0)
|
|
}
|
|
|
|
// MARK: - Encode → Decode Round-Trip
|
|
|
|
func testRoundTrip_AllTypes() throws {
|
|
let original: [MessageAttachment] = [
|
|
MessageAttachment(id: "1", preview: "tag::hash", blob: "data", type: .image),
|
|
MessageAttachment(id: "2", preview: "", blob: "", type: .messages),
|
|
MessageAttachment(id: "3", preview: "tag::500::doc.pdf", blob: "", type: .file),
|
|
MessageAttachment(id: "4", preview: "", blob: "", type: .avatar),
|
|
MessageAttachment(id: "5", preview: "60", blob: "", type: .call),
|
|
]
|
|
let data = try JSONEncoder().encode(original)
|
|
let decoded = try JSONDecoder().decode([MessageAttachment].self, from: data)
|
|
XCTAssertEqual(decoded, original)
|
|
}
|
|
|
|
// =========================================================================
|
|
// MARK: - Cross-Context Consistency
|
|
// =========================================================================
|
|
|
|
/// Verify all 3 contexts produce the same label for each type when text is empty.
|
|
func testAllContextsAgree_Photo() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .image)])
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 0)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Photo")
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Photo")
|
|
}
|
|
|
|
func testAllContextsAgree_File() {
|
|
// Note: reply bar shows filename, quote/chatlist show "File"
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 2)])
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "File")
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .file)]), "File")
|
|
}
|
|
|
|
func testAllContextsAgree_Avatar() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .avatar)])
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 3)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Avatar")
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Avatar")
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Avatar")
|
|
}
|
|
|
|
func testAllContextsAgree_Forward() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .messages)])
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 1)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Forwarded message")
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Forwarded message")
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Forwarded message")
|
|
}
|
|
|
|
func testAllContextsAgree_Call() {
|
|
let msg = makeMessage(attachments: [makeAttachment(type: .call)])
|
|
let reply = makeReply(attachments: [makeReplyAttachment(type: 4)])
|
|
XCTAssertEqual(replyPreviewText(for: msg), "Call")
|
|
XCTAssertEqual(replyQuotePreviewText(for: reply), "Call")
|
|
XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Call")
|
|
}
|
|
}
|