summaryrefslogtreecommitdiff
path: root/app/src/androidTest
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/androidTest')
-rw-r--r--app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt159
1 files changed, 159 insertions, 0 deletions
diff --git a/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt b/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt
new file mode 100644
index 0000000..67b3bf1
--- /dev/null
+++ b/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt
@@ -0,0 +1,159 @@
+package org.terst.feedreader.readability
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.runBlocking
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Maths rendering during extraction ("we should render tex", reported against lcamtuf's
+ * Substack).
+ *
+ * What the live page actually ships -- confirmed by fetching
+ * https://blog.coredump.cx/p/yours-truly -- is TeX SOURCE, in a text node, inside a
+ * `<div class="... latex-rendered" data-component-name="Latex">` wrapper: Substack renders
+ * it client-side with its own script, which never runs in this app. Readability keeps that
+ * text verbatim (it strips the class but not the text), so the reader showed the source.
+ *
+ * So extraction renders it: KaTeX (vendored in assets, never fetched) converts each
+ * expression to MathML before Readability runs, and the reader WebView -- still with
+ * JavaScript off -- renders the MathML natively.
+ *
+ * Needs a real WebView, so this is instrumented. Fixtures reproduce the exact markup shape
+ * from the live page rather than being a scraped copy of it (see ReaderExtractorFixtureTest
+ * for why).
+ */
+@RunWith(AndroidJUnit4::class)
+class ReaderExtractorMathTest {
+
+ private lateinit var server: MockWebServer
+
+ @Before
+ fun startServer() {
+ server = MockWebServer()
+ server.start()
+ }
+
+ @After
+ fun stopServer() {
+ server.shutdown()
+ }
+
+ private fun extract(html: String): ExtractedArticle? {
+ server.enqueue(MockResponse().setBody(html).setResponseCode(200))
+ val url = server.url("/article").toString()
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ return runBlocking { ReaderExtractor.extract(context, url) }
+ }
+
+ private val filler =
+ "Considered as a whole, an if-then statement cannot be true when its antecedent holds " +
+ "and its consequent does not, which is the whole of the trick being played here. "
+
+ // Substack's shape, verbatim from the live page: inline \( \) delimiters in a text node,
+ // inside a block-styled span inside the latex-rendered wrapper. Note the align block is
+ // ALSO wrapped in inline delimiters -- KaTeX refuses {align} outside display mode, so
+ // rendering has to fall back to display mode rather than give up.
+ private val substackFixture = """
+ <html><head><title>Yours truly</title></head>
+ <body>
+ <article>
+ <h1>Yours truly</h1>
+ <p>Imagine the following seemingly innocent sentence:</p>
+ <div class="pencraft latex-rendered" data-component-name="Latex">
+ <div class="pencraft"><span style="display:block">\(p = \text{“if }p\text{ is true, then pigs can fly”}\)</span></div>
+ </div>
+ <p>$filler</p>
+ <p>A slice of the set may look the following way:</p>
+ <div class="pencraft latex-rendered" data-component-name="Latex">
+ <div class="pencraft"><span style="display:block">\(\begin{align}
+t_{1012} &amp;= \text{“Cindy weighs 12 pounds.”} \\
+t_{1013} &amp;= \text{“Cindy doesn't weigh 13 pounds.”}
+\end{align}\)</span></div>
+ </div>
+ <p>$filler</p>
+ </article>
+ </body></html>
+ """.trimIndent()
+
+ // The other two conventional delimiter styles, and the one place maths must NOT be
+ // rendered: an article quoting TeX source on purpose, inside <pre><code>.
+ private val delimiterFixture = """
+ <html><head><title>Delimiter styles</title></head>
+ <body>
+ <article>
+ <h1>Delimiter styles</h1>
+ <p>$filler</p>
+ <p>The bracket form: \[ x_{1} + y^{2} \] and the dollar form: $$ a_{n} = b^{n} $$ in one paragraph.</p>
+ <p>$filler</p>
+ <p>To write that yourself, type:</p>
+ <pre><code>\(E = mc^{2}\)</code></pre>
+ <p>$filler</p>
+ </article>
+ </body></html>
+ """.trimIndent()
+
+ @Test
+ fun substackTexSourceIsRenderedToMathMl() {
+ val result = extract(substackFixture)
+
+ assertNotNull("extraction should succeed", result)
+ val content = result?.content.orEmpty()
+ assertTrue("both expressions should be rendered as MathML, got:\n$content", content.split("<math").size - 1 == 2)
+ assertFalse(
+ "no raw TeX delimiters should be left in the extracted content, got:\n$content",
+ content.contains("\\(") || content.contains("\\)")
+ )
+ assertTrue(
+ "the article's own text must survive alongside the maths",
+ content.contains("Imagine the following seemingly innocent sentence")
+ )
+ }
+
+ @Test
+ fun anAlignBlockFallsBackToDisplayMode() {
+ val result = extract(substackFixture)
+
+ val content = result?.content.orEmpty()
+ assertTrue(
+ "the {align} block must render as display maths (KaTeX rejects it inline), got:\n$content",
+ content.contains("display=\"block\"")
+ )
+ // KaTeX renders every space inside \text{...} as a non-breaking space, so the
+ // sentence is in there with U+00A0 between its words, not plain spaces.
+ assertTrue(
+ "the align block's own text should be rendered, got:\n$content",
+ content.replace("&nbsp;", " ").replace('\u00a0', ' ').contains("Cindy weighs 12 pounds")
+ )
+ }
+
+ @Test
+ fun bracketAndDollarDelimitersAreRenderedToo() {
+ val result = extract(delimiterFixture)
+
+ val content = result?.content.orEmpty()
+ assertTrue("both delimiter styles should render, got:\n$content", content.split("<math").size - 1 == 2)
+ assertFalse("the bracket delimiters should be consumed", content.contains("\\["))
+ assertFalse("the dollar delimiters should be consumed", content.contains("$$"))
+ assertTrue("surrounding prose must be preserved", content.contains("in one paragraph"))
+ }
+
+ @Test
+ fun texQuotedInACodeBlockIsLeftAsSource() {
+ val result = extract(delimiterFixture)
+
+ val content = result?.content.orEmpty()
+ assertTrue(
+ "TeX quoted inside <pre><code> is the point of the sentence and must stay source, got:\n$content",
+ content.contains("E = mc^{2}")
+ )
+ }
+}