summaryrefslogtreecommitdiff
path: root/app/src/test/java/org/terst/feedreader/readability/EvalOutcomeTest.kt
blob: 3e2b28fe7cbb3869e1b40511d88f34b3d8b5705d (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
package org.terst.feedreader.readability

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The decision ReaderExtractor makes about each `evaluateJavascript` result:
 * accept it, inject again because the page probably was not ready, or give up
 * with a reason. It used to live inline in a WebView callback, which meant the
 * only way to exercise it was on a device against a live page.
 *
 * The case that mattered and was never covered: a result that is perfectly
 * well-formed and non-blank, and is the page's cookie-consent banner rather than
 * the article. That counted as a success, got cached, and stayed cached.
 */
class EvalOutcomeTest {

    private val maxAttempts = 6

    private fun decide(article: ExtractedArticle?, attempt: Int, raw: String = "\"{}\"") =
        decideEvalOutcome(article, raw, attempt, maxAttempts)

    private fun article(content: String?, error: String? = null) =
        ExtractedArticle(title = "t", content = content, textContent = content, error = error)

    @Test
    fun `real content is accepted on the first attempt`() {
        val a = article("<p>" + "The actual article body. ".repeat(60) + "</p>")
        val decision = decide(a, attempt = 1)
        assertEquals(EvalDecision.Done(ExtractionResult.Success(a)), decision)
    }

    // Retrying cannot change Readability's opinion of the markup, so a thrown
    // error is final however early it arrives.
    @Test
    fun `a readability error is final immediately`() {
        val decision = decide(article(null, error = "TypeError: x is null"), attempt = 1)
        assertEquals(
            EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.ReadabilityError("TypeError: x is null"))),
            decision
        )
    }

    @Test
    fun `an undecodable result retries and then reports a bad result`() {
        assertEquals(EvalDecision.Retry, decide(null, attempt = 1, raw = "null"))
        val last = decide(null, attempt = maxAttempts, raw = "null")
        assertEquals(EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.BadResult("null"))), last)
    }

    @Test
    fun `blank content retries and then reports no content`() {
        assertEquals(EvalDecision.Retry, decide(article(""), attempt = 1))
        assertEquals(
            EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.NoContent)),
            decide(article(""), attempt = maxAttempts)
        )
    }

    // A cookie banner is exactly what a half-loaded page yields -- the banner is
    // injected early, the article body may not be in the DOM yet -- so the right
    // first move is the retry the extractor already does for an empty result.
    @Test
    fun `a consent notice retries rather than being accepted`() {
        val decision = decide(article(ConsentNoticeTest.EXTRACTED_COOKIE_NOTICE), attempt = 1)
        assertEquals(EvalDecision.Retry, decision)
    }

    @Test
    fun `a consent notice that survives every attempt is reported as a failure`() {
        val decision = decide(article(ConsentNoticeTest.EXTRACTED_COOKIE_NOTICE), attempt = maxAttempts)
        assertEquals(
            EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.ConsentInterstitial)),
            decision
        )
    }

    // The failure has to be distinguishable from "no content": one means the page
    // has no article, the other means the page would not show it without consent.
    @Test
    fun `the consent failure says what happened`() {
        val f: ExtractionFailure = ExtractionFailure.ConsentInterstitial
        assertTrue(f.userMessage().isNotBlank())
        assertTrue(f.diagnostic().isNotBlank())
        assertTrue(f.userMessage().lowercase().contains("cookie") || f.userMessage().lowercase().contains("consent"))
        assertTrue(f.userMessage() != ExtractionFailure.NoContent.userMessage())
    }

    /**
     * A fresh extraction is cleaned before it is accepted, so the widgets never reach the
     * screen and never reach `articles.extractedContent` either.
     */
    @Test
    fun `a successful extraction has its recirculation widgets stripped`() {
        val body = "<p>" + "Real article sentence. ".repeat(40) + "</p>"
        val widget = "<div data-mrf-recirculation=\"IL_x\"><p><h3><span>Trending</span></h3></p></div>"
        val article = ExtractedArticle(title = "T", content = body + widget + body)

        val decision = decideEvalOutcome(article, rawResult = "{}", attempt = 1, maxAttempts = 6)

        val content = ((decision as EvalDecision.Done).result as ExtractionResult.Success).article.content!!
        assertFalse("the stray header reached the caller: $content", content.contains("Trending"))
        assertTrue(content.contains("Real article sentence."))
    }
}