summaryrefslogtreecommitdiff
path: root/app/src/test/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/test/java')
-rw-r--r--app/src/test/java/org/terst/feedreader/data/ItemTitlesTest.kt179
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 &amp; Co&#39;s &quot;deal&quot; &mdash; 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&nbsp;two&nbsp;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"))
+ }
+}