diff options
Diffstat (limited to 'app/src/test/java/org/terst/feedreader')
| -rw-r--r-- | app/src/test/java/org/terst/feedreader/data/ItemTitlesTest.kt | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/app/src/test/java/org/terst/feedreader/data/ItemTitlesTest.kt b/app/src/test/java/org/terst/feedreader/data/ItemTitlesTest.kt new file mode 100644 index 0000000..63606ff --- /dev/null +++ b/app/src/test/java/org/terst/feedreader/data/ItemTitlesTest.kt @@ -0,0 +1,179 @@ +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 + * `<description>` and no `<title>` 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")) + } +} |
