import XCTest @testable import LChatCore // Parity gate for RichText.swift against web/static/format.js โ€” the SAME cases // as test/format.test.mjs, so a message formats identically in both feeds. If // one of these fails after a format.js change, update RichText.swift in the // same change (and vice versa). final class RichTextTests: XCTestCase { private func links(_ s: String) -> [String] { richTokens(s).compactMap { if case .link(_, let url) = $0 { return url.absoluteString } else { return nil } } } func testPlainTextPassesThrough() { XCTAssertEqual(richTokens("just a normal message"), [.text("just a normal message")]) } func testHttpsLinkSplits() { XCTAssertEqual(richTokens("see https://example.com/page now"), [ .text("see "), .link("https://example.com/page", URL(string: "https://example.com/page")!), .text(" now"), ]) } func testWwwGetsScheme() { XCTAssertEqual(links("go to www.example.com"), ["https://www.example.com"]) } func testTrailingPunctuationStripped() { XCTAssertEqual(links("look at https://example.com."), ["https://example.com"]) XCTAssertEqual(links("(https://example.com)"), ["https://example.com"]) } func testJavascriptSchemeIsNotALink() { XCTAssertEqual(links("javascript:alert(1)"), []) } func testMultipleLinks() { XCTAssertEqual(links("a https://one.com b http://two.com c"), ["https://one.com", "http://two.com"]) } func testBareDomainsWithKnownTLDs() { XCTAssertEqual(links("see rte.ie for news"), ["https://rte.ie"]) XCTAssertEqual(links("on guardian.co.uk today"), ["https://guardian.co.uk"]) XCTAssertEqual(links("rte.ie/news here"), ["https://rte.ie/news"]) } func testFileNamesAndEmailsAreNotLinks() { for s in ["run node.js today", "open index.html now", "see main.go there", "ping paul@rte.ie now"] { XCTAssertEqual(links(s), [], s) } } func testInlineFormatting() { XCTAssertEqual(richTokens("a **bold** move"), [.text("a "), .bold("bold"), .text(" move")]) XCTAssertEqual(richTokens("so *italic* here"), [.text("so "), .italic("italic"), .text(" here")]) XCTAssertEqual(richTokens("it exists _to test_ :-/"), [.text("it exists "), .italic("to test"), .text(" :-/")]) XCTAssertEqual(richTokens("some `code` inline"), [.text("some "), .code("code"), .text(" inline")]) XCTAssertEqual(richTokens("well ~nope~ then"), [.text("well "), .strike("nope"), .text(" then")]) } // test/format.test.mjs "triple asterisk is combined bold italic" func testTripleAsteriskIsCombinedBoldItalic() { XCTAssertEqual(richTokens("***both***"), [.boldItalic("both")]) XCTAssertEqual(richTokens("a ***b c*** d"), [.text("a "), .boldItalic("b c"), .text(" d")]) // Neighbours don't bleed: bi next to bold, and markers still can't span lines. XCTAssertEqual(richTokens("***x*** **y**"), [.boldItalic("x"), .text(" "), .bold("y")]) XCTAssertEqual(richTokens("***a\nb***"), [.text("***a\nb***")]) } func testUnderscoreItalicsNeedWordBoundaries() { // file_names must not italicize (format.js's lookbehind/lookahead). XCTAssertEqual(richTokens("see file_name_here"), [.text("see file_name_here")]) } func testLinksAreNeverFormattedInside() { // The underscore inside the URL must not become italics. XCTAssertEqual(links("https://example.com/a_b_c"), ["https://example.com/a_b_c"]) } // ---- blockTokens โ€” the eight cases from test/format.test.mjs ---------- func testBlockPlainTextIsOneUntouchedBlock() { XCTAssertEqual(blockTokens("hello\n\nworld"), [.p("hello\n\nworld")]) XCTAssertEqual(blockTokens(""), [.p("")]) } func testBlockFencedCodeMarkersVerbatim() { XCTAssertEqual(blockTokens("look:\n```\nif (a > b) { *notBold* }\n```\nnice"), [ .p("look:"), .code("if (a > b) { *notBold* }", lang: ""), .p("nice"), ]) } func testBlockFenceCapturesLanguage() { XCTAssertEqual(blockTokens("```js\nconst x = 1;\n```"), [.code("const x = 1;", lang: "js")]) } func testBlockUnterminatedFenceRunsToEnd() { XCTAssertEqual(blockTokens("```\nline one\nline two"), [.code("line one\nline two", lang: "")]) } func testBlockInlineBackticksAreNotAFence() { XCTAssertEqual(blockTokens("```js``` opens a fence"), [.p("```js``` opens a fence")]) } func testBlockConsecutiveQuoteLinesMerge() { XCTAssertEqual(blockTokens("> two roads diverged\n> in a yellow wood\nso true"), [ .quote("two roads diverged\nin a yellow wood"), .p("so true"), ]) } func testBlockBareQuoteContinuesAsBlankLine() { XCTAssertEqual(blockTokens("> first\n>\n> second"), [.quote("first\n\nsecond")]) } func testBlockMidLineAngleIsNotAQuote() { XCTAssertEqual(blockTokens("5 > 3 and 2 > 1"), [.p("5 > 3 and 2 > 1")]) } func testBlockBlankLinesAroundBlocksDropped() { XCTAssertEqual(blockTokens("intro\n\n> quoted\n\noutro"), [ .p("intro"), .quote("quoted"), .p("outro"), ]) } // ---- youtubeId โ€” the shapes from test/format.test.mjs ------------------ func testYouTubeIdParsesCommonShapes() { func yt(_ s: String) -> (String, Int)? { youtubeId(URL(string: s)!).map { ($0.id, $0.start) } } XCTAssertEqual(yt("https://www.youtube.com/watch?v=dQw4w9WgXcQ")?.0, "dQw4w9WgXcQ") XCTAssertEqual(yt("https://youtu.be/dQw4w9WgXcQ")?.0, "dQw4w9WgXcQ") XCTAssertEqual(yt("https://m.youtube.com/watch?v=dQw4w9WgXcQ&pp=xyz")?.0, "dQw4w9WgXcQ") XCTAssertEqual(yt("https://www.youtube.com/shorts/dQw4w9WgXcQ")?.0, "dQw4w9WgXcQ") XCTAssertEqual(yt("https://www.youtube.com/embed/dQw4w9WgXcQ")?.0, "dQw4w9WgXcQ") XCTAssertEqual(yt("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90")?.1, 90) XCTAssertEqual(yt("https://youtu.be/dQw4w9WgXcQ?t=1h2m3s")?.1, 3723) XCTAssertNil(yt("https://example.com/watch?v=dQw4w9WgXcQ")) XCTAssertNil(yt("https://www.youtube.com/watch?v=short")) } func testEmojiOnlyCount() { XCTAssertEqual(emojiOnlyCount("๐Ÿ‘"), 1) XCTAssertEqual(emojiOnlyCount("๐Ÿ˜€๐Ÿ˜‚"), 2) XCTAssertEqual(emojiOnlyCount("๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰"), 3) XCTAssertEqual(emojiOnlyCount("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"), 1, "ZWJ family is one emoji") XCTAssertEqual(emojiOnlyCount("๐Ÿ‘๐Ÿฝ"), 1, "skin tone is one emoji") XCTAssertEqual(emojiOnlyCount("๐Ÿ‡ฌ๐Ÿ‡ง"), 1, "flag is one emoji") XCTAssertEqual(emojiOnlyCount("๐Ÿ‘ ๐Ÿ‘"), 2, "whitespace between emoji allowed") XCTAssertEqual(emojiOnlyCount("๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘"), 0, "more than 3 renders normally") XCTAssertEqual(emojiOnlyCount("hi ๐Ÿ‘"), 0) XCTAssertEqual(emojiOnlyCount("๐Ÿ‘ nice"), 0) XCTAssertEqual(emojiOnlyCount("plain text"), 0) XCTAssertEqual(emojiOnlyCount(""), 0) } // The web twin's regression (format.test.mjs): emphasis markers around a // URL must never ride into the href โ€” "**github.com/apps/x**" gets a // clean link, stars left as literal text. func testEmphasisMarkersStayOutOfHrefs() { let toks = tokenizeText("go to **github.com/apps/eleven-messenger** and install") let link = toks.first(where: { $0.href != nil }) XCTAssertEqual(link?.text, "github.com/apps/eleven-messenger") XCTAssertEqual(link?.href?.absoluteString.contains("*"), false) XCTAssertEqual(toks.map(\.text).joined(), "go to **github.com/apps/eleven-messenger** and install") let toks2 = tokenizeText("see https://docs.github.com/rest/issues#list**") let link2 = toks2.first(where: { $0.href != nil }) XCTAssertEqual(link2?.text, "https://docs.github.com/rest/issues#list") } }