summaryrefslogtreecommitdiff
path: root/app/src/test/java/org/terst/feedreader/readability/RecirculationWidgetsTest.kt
blob: 0926b80e6d56c4f408adcae640bc74e517f52904 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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 =
        """<div data-mrf-recirculation="IL_gKyXYBmnRnamdDxChJw6lw" data-testid="text-html" """ +
            """data-mrf-experience="IL_gKyXYBmnRnamdDxChJw6lw"><p><h3><span>Trending</span></h3></p></div>"""

    @Test
    fun `strips a recirculation widget from the middle of an article`() {
        val html = "<p>Real sentence one.</p>$realWidget<p>Real sentence two.</p>"

        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 = "<p>One.</p>$realWidget<p>Two.</p>$realWidget<p>Three.</p>"

        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 = "<div><p>Nothing to do here.</p><h2>A real heading</h2><p>More.</p></div>"

        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 = """<div data-mrf-recirculation="X"><p>$paragraph</p></div>"""

        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 = "<p>a</p><div data-mrf-recirculation='Y'><h3>Trending</h3></div><p>b</p>"

        assertFalse(RecirculationWidgets.strip(html)!!.contains("Trending"))
    }

    @Test
    fun `survives a nested div inside the widget`() {
        val html = "<p>a</p><div data-mrf-recirculation=\"Z\"><div><h3>Trending</h3></div></div><p>b</p>"

        val cleaned = RecirculationWidgets.strip(html)!!
        assertFalse(cleaned.contains("Trending"))
        assertTrue(cleaned.contains("<p>a</p>"))
        assertTrue(cleaned.contains("<p>b</p>"))
    }

    @Test
    fun `an unclosed widget div does not eat the rest of the article`() {
        val html = "<p>Before.</p><div data-mrf-recirculation=\"W\"><h3>Trending</h3><p>After.</p>"

        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
        )
    }
}