| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // π€ Sidebar snippet (previewText) must mirror the web (previewText in |
| 5 | // shell.js): a non-image attachment reads as a file chip, not "π· Photo". |
| 6 | final class PreviewTextTests: XCTestCase { |
| 7 | private func att(_ name: String, _ type: String?) -> Attachment { |
| 8 | Attachment(id: "b", name: name, type: type) |
| 9 | } |
| 10 | |
| 11 | func testTextTakesPrecedence() { |
| 12 | var p = MessagePayload() |
| 13 | p.text = " hello world \n" |
| 14 | p.attachments = [att("photo.jpg", "image/jpeg")] |
| 15 | XCTAssertEqual(previewText(kind: "dm", payload: p), "hello world") |
| 16 | } |
| 17 | |
| 18 | func testSingleImage() { |
| 19 | var p = MessagePayload() |
| 20 | p.attachments = [att("photo.jpg", "image/jpeg")] |
| 21 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π· Photo") |
| 22 | } |
| 23 | |
| 24 | func testMultipleImages() { |
| 25 | var p = MessagePayload() |
| 26 | p.attachments = [att("a.jpg", "image/jpeg"), att("b.png", "image/png")] |
| 27 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π· 2 photos") |
| 28 | } |
| 29 | |
| 30 | func testSingleFileIsNotAPhoto() { |
| 31 | var p = MessagePayload() |
| 32 | p.attachments = [att("report.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")] |
| 33 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π report.docx") |
| 34 | } |
| 35 | |
| 36 | func testMultipleFiles() { |
| 37 | var p = MessagePayload() |
| 38 | p.attachments = [att("a.docx", "application/msword"), att("b.pdf", "application/pdf")] |
| 39 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π 2 files") |
| 40 | } |
| 41 | |
| 42 | func testMixedImageAndFileFallsToFiles() { |
| 43 | var p = MessagePayload() |
| 44 | p.attachments = [att("a.jpg", "image/jpeg"), att("b.pdf", "application/pdf")] |
| 45 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π 2 files") |
| 46 | } |
| 47 | |
| 48 | // A typeless attachment (legacy) isn't an image β file chip with its name. |
| 49 | func testUntypedAttachment() { |
| 50 | var p = MessagePayload() |
| 51 | p.attachments = [att("mystery.bin", nil)] |
| 52 | XCTAssertEqual(previewText(kind: "dm", payload: p), "π mystery.bin") |
| 53 | } |
| 54 | |
| 55 | func testGroupPrefix() { |
| 56 | var p = MessagePayload() |
| 57 | p.name = "Ada" |
| 58 | p.attachments = [att("report.docx", "application/msword")] |
| 59 | XCTAssertEqual(previewText(kind: "group", payload: p), "Ada: π report.docx") |
| 60 | } |
| 61 | |
| 62 | // A reaction has no text of its own β the row used to go blank (in a group, |
| 63 | // a bare "Ada: "). It says what happened instead, quoting the snippet the |
| 64 | // reacting client stamped inside the ciphertext. Web twin: reactionLine in |
| 65 | // format.js + the reaction branch in shell.js previewText. |
| 66 | private func reaction(_ name: String, _ emoji: String, on: Bool, target: String?) -> MessagePayload { |
| 67 | var p = MessagePayload() |
| 68 | p.type = "reaction" |
| 69 | p.name = name |
| 70 | p.emoji = emoji |
| 71 | p.on = on |
| 72 | p.targetSid = 12345 |
| 73 | p.targetText = target |
| 74 | return p |
| 75 | } |
| 76 | |
| 77 | func testReactionQuotesItsTarget() { |
| 78 | let p = reaction("Ada", "π", on: true, target: "buy milk") |
| 79 | XCTAssertEqual(previewText(kind: "dm", payload: p), "Ada reacted π to βbuy milkβ") |
| 80 | } |
| 81 | |
| 82 | // It names the actor itself, so the group prefix never doubles up. |
| 83 | func testReactionTakesNoGroupPrefix() { |
| 84 | let p = reaction("Ada", "π", on: true, target: "buy milk") |
| 85 | XCTAssertEqual(previewText(kind: "group", payload: p), "Ada reacted π to βbuy milkβ") |
| 86 | } |
| 87 | |
| 88 | func testMyOwnReactionSaysYou() { |
| 89 | let p = reaction("Ada", "π", on: true, target: "shipped it") |
| 90 | XCTAssertEqual(previewText(kind: "group", payload: p, mine: true), "You reacted π to βshipped itβ") |
| 91 | } |
| 92 | |
| 93 | // An un-react speaks too: on reload this event is all the sidebar has, so |
| 94 | // silence would mean a different row after a refresh than the one shown live. |
| 95 | func testUnReactSpeaks() { |
| 96 | let p = reaction("Bob", "β€οΈ", on: false, target: "shipped it π") |
| 97 | XCTAssertEqual(previewText(kind: "dm", payload: p), "Bob removed β€οΈ from βshipped it πβ") |
| 98 | } |
| 99 | |
| 100 | // Reactions from clients that predate targetText (and every reaction |
| 101 | // already in a database) carry no snippet β the bare form still reads. |
| 102 | func testReactionWithoutASnippet() { |
| 103 | XCTAssertEqual(previewText(kind: "dm", payload: reaction("Ada", "π", on: true, target: nil)), |
| 104 | "Ada reacted π") |
| 105 | XCTAssertEqual(previewText(kind: "dm", payload: reaction("Ada", "π", on: false, target: nil)), |
| 106 | "Ada removed π") |
| 107 | } |
| 108 | |
| 109 | func testReactionQuoteIsClamped() { |
| 110 | let p = reaction("Ada", "π", on: true, target: String(repeating: "a", count: 200)) |
| 111 | let line = previewText(kind: "dm", payload: p) |
| 112 | XCTAssertTrue(line.hasPrefix("Ada reacted π to β")) |
| 113 | XCTAssertTrue(line.hasSuffix("β¦β")) |
| 114 | XCTAssertLessThan(line.count, 70, "the sidebar line stays one line's worth") |
| 115 | } |
| 116 | |
| 117 | // The caller resolves the actor by uid (ThreadStore's displayLabels β my |
| 118 | // name for them, #577), and that wins over the payload's snapshot. Without |
| 119 | // it, a reaction from a device that stamped no screen name β common on real |
| 120 | // data β reads "Someone reacted π". |
| 121 | func testActorResolvesByUidOverThePayloadName() { |
| 122 | var p = reaction("", "π", on: true, target: nil) |
| 123 | p.name = "" |
| 124 | XCTAssertEqual(previewText(kind: "group", payload: p, actor: "Paul"), "Paul reacted π") |
| 125 | } |
| 126 | |
| 127 | func testResolvedActorBeatsAStalePayloadName() { |
| 128 | let p = reaction("Old Name", "π", on: true, target: "buy milk") |
| 129 | XCTAssertEqual(previewText(kind: "dm", payload: p, actor: "My Name For Them"), |
| 130 | "My Name For Them reacted π to βbuy milkβ") |
| 131 | } |
| 132 | |
| 133 | // Nobody to resolve (they left the space, and the payload named no one). |
| 134 | func testReactionWithoutANameSaysSomeone() { |
| 135 | var p = reaction("", "π", on: true, target: "buy milk") |
| 136 | p.name = nil |
| 137 | XCTAssertEqual(previewText(kind: "dm", payload: p), "Someone reacted π to βbuy milkβ") |
| 138 | } |
| 139 | |
| 140 | // The group prefix takes the same by-uid resolution as the reaction line: |
| 141 | // my name for them, so the row survives a rename and never renders a bare |
| 142 | // ": hello" for a sender whose device stamped no screen name. Web twin: |
| 143 | // previewText in shell.js resolves `who` once and uses it for both. |
| 144 | func testGroupPrefixUsesTheResolvedActor() { |
| 145 | var p = MessagePayload() |
| 146 | p.name = "Old Name" |
| 147 | p.text = "hello" |
| 148 | XCTAssertEqual(previewText(kind: "group", payload: p, actor: "My Name For Them"), |
| 149 | "My Name For Them: hello") |
| 150 | } |
| 151 | |
| 152 | func testGroupPrefixFallsBackToThePayloadName() { |
| 153 | var p = MessagePayload() |
| 154 | p.name = "Ada" |
| 155 | p.text = "hello" |
| 156 | XCTAssertEqual(previewText(kind: "group", payload: p), "Ada: hello") |
| 157 | } |
| 158 | |
| 159 | // Nothing to resolve and nothing stamped: no stray colon. |
| 160 | func testGroupPrefixIsSkippedWithNoNameAtAll() { |
| 161 | var p = MessagePayload() |
| 162 | p.text = "hello" |
| 163 | XCTAssertEqual(previewText(kind: "group", payload: p), "hello") |
| 164 | } |
| 165 | |
| 166 | // An action line names its own actor β prefixing it doubles the name up. |
| 167 | func testActionLineTakesNoGroupPrefix() { |
| 168 | var p = MessagePayload() |
| 169 | p.name = "Ada" |
| 170 | p.text = "waves" |
| 171 | p.action = true |
| 172 | XCTAssertEqual(previewText(kind: "group", payload: p, actor: "Ada"), "waves") |
| 173 | } |
| 174 | } |
| 175 | |