diff options
Diffstat (limited to 'app/src/androidTest/java')
| -rw-r--r-- | app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt | 159 |
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} &= \text{“Cindy weighs 12 pounds.”} \\ +t_{1013} &= \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(" ", " ").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}") + ) + } +} |
