package org.terst.feedreader.readability import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * Promo blocks a publisher's widget platform injects into the middle of the article body. * * Reported live on West Hawaii Today: "articles get a bunch of stray 'Trending' headers". * Confirmed on 9 of the 12 articles in its feed, two per article. */ class RecirculationWidgetsTest { /** The exact markup that survives extraction, from a West Hawaii Today article. */ private val realWidget = """

Trending

""" @Test fun `strips a recirculation widget from the middle of an article`() { val html = "

Real sentence one.

$realWidget

Real sentence two.

" val cleaned = RecirculationWidgets.strip(html)!! assertFalse("the stray header is still there: $cleaned", cleaned.contains("Trending")) assertTrue(cleaned.contains("Real sentence one.")) assertTrue(cleaned.contains("Real sentence two.")) } @Test fun `strips every widget, not just the first`() { val html = "

One.

$realWidget

Two.

$realWidget

Three.

" val cleaned = RecirculationWidgets.strip(html)!! assertEquals(0, Regex("Trending").findAll(cleaned).count()) assertTrue(cleaned.contains("Three.")) } @Test fun `leaves an article with no widgets exactly as it was`() { val html = "

Nothing to do here.

A real heading

More.

" assertEquals(html, RecirculationWidgets.strip(html)) } @Test fun `never removes a block long enough to be real writing`() { val paragraph = "Beaches across the island lost coral cover. ".repeat(20) val html = """

$paragraph

""" assertTrue( "a block this long is article text whatever attribute it carries", RecirculationWidgets.strip(html)!!.contains("Beaches across the island") ) } @Test fun `handles an unquoted or single-quoted attribute`() { val html = "

a

Trending

b

" assertFalse(RecirculationWidgets.strip(html)!!.contains("Trending")) } @Test fun `survives a nested div inside the widget`() { val html = "

a

Trending

b

" val cleaned = RecirculationWidgets.strip(html)!! assertFalse(cleaned.contains("Trending")) assertTrue(cleaned.contains("

a

")) assertTrue(cleaned.contains("

b

")) } @Test fun `an unclosed widget div does not eat the rest of the article`() { val html = "

Before.

Trending

After.

" assertTrue( "a malformed widget must not take the article with it", RecirculationWidgets.strip(html)!!.contains("Before.") ) } @Test fun `null and blank are left alone`() { assertEquals(null, RecirculationWidgets.strip(null)) assertEquals("", RecirculationWidgets.strip("")) } /** * The whole extracted article, captured from the live page on 2026-10-21 by running the * vendored Readability.js over it exactly as the app does. * * The unit cases above use the widget markup on its own, which proves the matcher but not * that it meets reality in one piece. This is the article Peter would have been reading. */ @Test fun `the real extracted article loses both widgets and keeps its text`() { val extracted = checkNotNull( javaClass.getResourceAsStream("/west-hawaii-today-extracted.html") ).bufferedReader().use { it.readText() } assertEquals("fixture should carry the reported bug", 2, Regex("Trending").findAll(extracted).count()) val cleaned = RecirculationWidgets.strip(extracted)!! assertEquals(0, Regex("Trending").findAll(cleaned).count()) assertTrue(cleaned.contains("Leavitt officially stepped down as White House press secretary")) assertTrue(cleaned.contains("The New York Times")) assertTrue( "barely any of the article should have gone with the widgets", cleaned.length > extracted.length - 600 ) } }