package org.terst.feedreader.data import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test /** * A title for an item that doesn't have one. * * RSS only requires an item to carry a title *or* a description, and Mastodon's tag feeds * exercise that: every one of the 20 items in `mastodon.social/tags/giftlink.rss` has a * `` and no `` at all. Subscribing to one produced a list of blank rows. */ class ItemTitlesTest { /** Verbatim from the #giftlink feed, 2026-10-22, as the XML parser hands it over. */ private val mastodonPost = """<p>This is a nice story, with lovely photos. <a href="https://infosec.exchange/tags/giftlink" """ + """class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>giftlink</span></a> </p>""" + """<p><a href="https://www.nytimes.com/2026/10/20/us/catherine-breed.html?unlocked_article_code=1.KlE" """ + """rel="nofollow noopener" target="_blank"><span class="invisible">https://www.</span>""" + """<span class="ellipsis">nytimes.com/2026/10/20/us/cath</span></a></p>""" @Test fun `a real title is left exactly as it is`() { assertEquals( "The Flipbook of the Rings", ItemTitles.titleFor("The Flipbook of the Rings", "<p>anything</p>", "https://kottke.org/x") ) } @Test fun `a title of nothing but whitespace counts as absent`() { assertEquals("Hello there.", ItemTitles.titleFor(" \n ", "<p>Hello there.</p>", "https://x/y")) } @Test fun `a titleless post is named by its own first sentence`() { assertEquals( "This is a nice story, with lovely photos.", ItemTitles.titleFor("", mastodonPost, "https://infosec.exchange/@cremevax/117483063928862987") ) } @Test fun `markup never reaches the title`() { val title = ItemTitles.titleFor("", mastodonPost, "https://x/y") assertTrue("got: $title", !title.contains("<") && !title.contains(">")) } @Test fun `html entities are resolved, since a title is shown as plain text`() { assertEquals( "Smith & Co's \"deal\" — done", ItemTitles.titleFor("", "<p>Smith & Co's "deal" — done</p>", "https://x/y") ) } @Test fun `a non-breaking space is a space, not a stray glyph`() { assertEquals("One two three", ItemTitles.titleFor("", "<p>One two three</p>", "https://x/y")) } @Test fun `runs of whitespace collapse, so a title stays one line`() { assertEquals("One two", ItemTitles.titleFor("", "<p>One</p>\n\n <p>two</p>", "https://x/y")) } @Test fun `a long post is cut at a word boundary and marked as cut`() { val long = "<p>" + "banana ".repeat(60) + "</p>" val title = ItemTitles.titleFor("", long, "https://x/y") assertTrue("should be cut: ${title.length}", title.length <= ItemTitles.MAX_LENGTH + 1) assertTrue("should say it was cut: $title", title.endsWith("…")) assertTrue("should not cut mid-word: $title", title.startsWith("banana banana")) assertTrue("should not cut mid-word: $title", !title.contains("banan…")) } @Test fun `a very long first sentence is cut rather than used whole`() { val long = "<p>" + "word ".repeat(80) + ". Second sentence.</p>" assertTrue(ItemTitles.titleFor("", long, "https://x/y").length <= ItemTitles.MAX_LENGTH + 1) } @Test fun `a post with no words falls back to its link`() { val title = ItemTitles.titleFor("", """<p><img src="https://x/a.jpg"></p>""", "https://infosec.exchange/@a/1") assertEquals("infosec.exchange/@a/1", title) } @Test fun `no title, no content and no link still gives something to tap`() { assertEquals("Untitled", ItemTitles.titleFor("", null, "")) } @Test fun `null content falls back to the link`() { assertEquals("example.com/a", ItemTitles.titleFor("", null, "https://example.com/a")) } @Test fun `a trailing slash is not worth showing in a title`() { assertEquals("example.com/a", ItemTitles.titleFor("", null, "https://example.com/a/")) } @Test fun `a script or style block contributes no words`() { val html = "<style>p{color:red}</style><script>var x=1;</script><p>Actual words.</p>" assertEquals("Actual words.", ItemTitles.titleFor("", html, "https://x/y")) } /** * Mastodon splits a hashtag and a URL across inline spans -- `#<span>giftlink</span>` and * `<span class="invisible">https://</span><span class="ellipsis">wapo.st/x</span>`. Turning * every tag into a space broke both apart, which showed live as titles reading * "# HumaneRights # USPolitics" and "the loophole https:// wapo.". */ @Test fun `an inline tag does not insert a space into a word`() { assertEquals( "Tagged #giftlink today.", ItemTitles.titleFor("", "<p>Tagged #<span>giftlink</span> today.</p>", "https://x/y") ) } @Test fun `a url split across spans stays one url`() { val html = """<p>Read <a href="https://wapo.st/x"><span class="invisible">https://</span>""" + """<span class="ellipsis">wapo.st/x</span></a> now</p>""" assertEquals("Read https://wapo.st/x now", ItemTitles.titleFor("", html, "https://x/y")) } @Test fun `a block boundary still separates words`() { assertEquals("One two", ItemTitles.titleFor("", "<p>One</p><p>two</p>", "https://x/y")) } @Test fun `a line break separates words`() { assertEquals("One two", ItemTitles.titleFor("", "One<br>two", "https://x/y")) } @Test fun `a dot inside a domain does not end the sentence`() { assertEquals( "The $580 loophole https://wapo.st/abc is open.", ItemTitles.titleFor("", "<p>The $580 loophole https://wapo.st/abc is open. And more.</p>", "https://x/y") ) } @Test fun `a sentence still ends at a full stop followed by a space`() { assertEquals( "The first sentence here.", ItemTitles.titleFor("", "<p>The first sentence here. Second one.</p>", "https://x/y") ) } @Test fun `a two-word opening fragment is not a title on its own`() { // "Wow." says nothing. Keep reading rather than name the article after an interjection. assertEquals( "Wow. Here is what the article actually says.", ItemTitles.titleFor("", "<p>Wow. Here is what the article actually says.</p>", "https://x/y") ) } @Test fun `a sentence ending the text needs no trailing space`() { assertEquals("Only sentence here.", ItemTitles.titleFor("", "<p>Only sentence here.</p>", "https://x/y")) } }