protected String plainText() {
StringBuilder text = new StringBuilder();
- for (Comic newComic : newComics) {
+ List<Comic> comicsToShow = new ArrayList<>(comics.stream().filter(newComics::contains).toList());
+ Collections.reverse(comicsToShow);
+ for (Comic newComic : comicsToShow) {
text.append("Comic Found: ").append(newComic.title()).append("\n\n");
for (Strip strip : newComic) {
text.append("Image: ").append(strip.imageUrl()).append("\n");
StringBuilder html = new StringBuilder();
html.append("<body>");
- for (Comic newComic : newComics) {
+ List<Comic> latestComics = new ArrayList<>(comics());
+ Collections.reverse(latestComics);
+
+ for (Comic newComic : latestComics.stream().filter(newComics::contains).toList()) {
generateComicHtml(html, newComic);
}
- List<Comic> latestComics = new ArrayList<>(comics());
- Collections.reverse(latestComics);
int comicCount = 0;
for (Comic comic : latestComics) {
if (newComics.contains(comic)) {
}
private void generateComicHtml(StringBuilder html, Comic comic) {
- html.append("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
+ html.append("<div>").append("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
for (Strip strip : comic) {
html.append("<div><img src=\"").append(StringEscapeUtils.escapeHtml4(strip.imageUrl()));
html.append("\" alt=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
html.append("\" title=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
- html.append("\"></div>\n");
+ html.append("\" /></div>\n");
html.append("<div>").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("</div>\n");
}
+ html.append("</div>");
}
/**
--- /dev/null
+package net.pterodactylus.rhynodge.states
+
+import java.io.StringReader
+import javax.xml.parsers.DocumentBuilderFactory
+import net.pterodactylus.rhynodge.Reaction
+import net.pterodactylus.rhynodge.states.ComicState.Comic
+import net.pterodactylus.rhynodge.states.ComicState.Strip
+import net.pterodactylus.util.dom.XPathEvaluator
+import net.pterodactylus.util.test.containsNode
+import net.pterodactylus.util.test.isImageNode
+import net.pterodactylus.util.test.isTextNode
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.not
+import org.junit.jupiter.api.Test
+import org.xml.sax.InputSource
+
+class ComicStateTest {
+
+ @Test
+ fun `can create comic state`() {
+ ComicState(emptySet())
+ }
+
+ @Test
+ fun `comic state is empty if list of comics is empty`() {
+ assertThat(ComicState(emptyList()).isEmpty, equalTo(true))
+ }
+
+ @Test
+ fun `comic state is not empty if list of comics contains a comic`() {
+ assertThat(ComicState(listOf(Comic(""))).isEmpty, equalTo(false))
+ }
+
+ @Test
+ fun `comic state without list of new comics is not triggered`() {
+ assertThat(ComicState(emptyList()).triggered(), equalTo(false))
+ }
+
+ @Test
+ fun `comic state with empty list of new comics is not triggered`() {
+ assertThat(ComicState(emptyList(), emptyList()).triggered(), equalTo(false))
+ }
+
+ @Test
+ fun `comic state with new comics is triggered`() {
+ assertThat(ComicState(emptyList(), listOf(Comic(""))).triggered(), equalTo(true))
+ }
+
+ @Test
+ fun `summary contains reaction's name`() {
+ assertThat(ComicState(emptyList()).summary(reaction), containsString("foo"))
+ }
+
+ @Test
+ fun `output's summary contains reaction's name`() {
+ val output = ComicState(emptyList()).output(reaction)
+ assertThat(output.summary(), containsString("foo"))
+ }
+
+ @Test
+ fun `output's text output contains only new comics`() {
+ val newComics = listOf(
+ Comic("New 1"),
+ Comic("New 2"),
+ )
+ val output = ComicState(newComics.plusElement(Comic("Old 1")), newComics).output(reaction)
+ assertThat(output.text("text/plain"), allOf(
+ containsString("Comic Found: New 1"),
+ containsString("Comic Found: New 2"),
+ not(containsString("Comic Found: Old 1")),
+ ))
+ }
+
+ @Test
+ fun `output's plain text contains all strips of comic`() {
+ val comics = listOf(
+ Comic("New 1").apply {
+ add(Strip("u1", "c1"))
+ },
+ Comic("New 2").apply {
+ add(Strip("u2.1", "c2.1"))
+ add(Strip("u2.2", "c2.2"))
+ }
+ )
+ val output = ComicState(comics, comics).output(reaction)
+ assertThat(output.text("text/plain"), allOf(
+ containsString("Comic Found: New 1\n\nImage: u1\nComment: c1"),
+ containsString("Comic Found: New 2\n\nImage: u2.1\nComment: c2.1\nImage: u2.2\nComment: c2.2"),
+ ))
+ }
+
+ @Test
+ fun `output's plain text contains comics in correct order`() {
+ val comics = (1..9).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ val output = ComicState(comics, comics).output(reaction)
+ val positions = (1..9).map { index -> output.text("text/plain").indexOf("Comic $index") }
+ assertThat(positions, contains(*positions.sortedDescending().toTypedArray()))
+ }
+
+ @Test
+ fun `comment is omitted if blank`() {
+ val comic = Comic("Comic 2").apply {
+ add(Strip("u2.1", " "))
+ add(Strip("u2.2", "c2.2"))
+ }
+ val output = ComicState(listOf(comic), listOf(comic)).output(reaction)
+ assertThat(output.text("text/plain"), allOf(
+ containsString("Comic Found: Comic 2\n\nImage: u2.1\nImage: u2.2\nComment: c2.2"),
+ ))
+ }
+
+ @Test
+ fun `comic is rendered correctly in output's html`() {
+ val comic = Comic("Comic 1").apply {
+ add(Strip("u1.1", "c1.1"))
+ add(Strip("u1.2", "c1.2"))
+ }
+ renderHtmlAndVerify(ComicState(listOf(comic), listOf(comic))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("(//body//h1)[1]/text()|(//body//h1)[1]/following-sibling::div"), contains(
+ isTextNode("Comic 1"), containsNode(isImageNode("u1.1", "c1.1", "c1.1")), isTextNode("c1.1"), containsNode(isImageNode("u1.2", "c1.2", "c1.2")), isTextNode("c1.2"),
+ ))
+ }
+ }
+
+ @Test
+ fun `output's html contains all new comics`() {
+ val comics = (1..9).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ renderHtmlAndVerify(ComicState(comics, comics)) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("//body//h1/text()"), containsInAnyOrder(
+ (1..9).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ }
+ }
+
+ @Test
+ fun `output's html contains comics in reverse order`() {
+ val comics = (1..7).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ renderHtmlAndVerify(ComicState(comics, comics)) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("//body//h1/text()"), contains(
+ (7 downTo 1).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ }
+ }
+
+ @Test
+ fun `output's html contains new comics before old comics`() {
+ val comics = (1..6).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ val newComics = (1..3).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ renderHtmlAndVerify(ComicState(comics, newComics)) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("//body//h1/text()").subList(0, 3), containsInAnyOrder(
+ (1..3).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ assertThat(xPathEvaluator.asNodeList("//body//h1/text()").subList(3, 6), containsInAnyOrder(
+ (4..6).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ }
+ }
+
+ @Test
+ fun `output's html contains new comics in list order`() {
+ val comics = (1..9).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ renderHtmlAndVerify(ComicState(comics, comics.subList(7, 9))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("(//body//h1)[position()=1 or position()=2]/text()"), contains(
+ (9 downTo 8).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ }
+ }
+
+ @Test
+ fun `output's html contains new comics plus at most seven old ones`() {
+ val comics = (1..9).map { index -> Comic("Comic $index").apply { add(Strip("u$index", "c$index")) } }
+ renderHtmlAndVerify(ComicState(comics, listOf(comics.last()))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("//body//h1/text()"), contains(
+ (9 downTo 2).map { index -> "Comic $index" }.map(::isTextNode)
+ ))
+ }
+ }
+
+ private fun renderHtmlAndVerify(comicState: ComicState, xPathEvaluator: (xPathEvaluator: XPathEvaluator) -> Unit) {
+ val output = comicState.output(reaction)
+ val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(output.text("text/html"))))
+ xPathEvaluator(XPathEvaluator(document))
+ }
+
+}
+
+private val reaction = Reaction("foo", null, null, null)
package net.pterodactylus.util.test
+import net.pterodactylus.util.dom.toList
import org.hamcrest.Description
+import org.hamcrest.Matcher
import org.hamcrest.TypeSafeDiagnosingMatcher
import org.w3c.dom.Node
description.appendText("text node with text ").appendValue(text)
}
}
+
+/**
+ * Creates a Hamcrest [matcher][Matcher] that matches [nodes][Node] based on
+ * whether they contain a singular element node matching the given
+ * [nodeMatcher].
+ *
+ * @param[nodeMatcher] The matcher for the contained node
+ * @return A [matcher][Matcher] for [nodes][Node]
+ */
+fun containsNode(nodeMatcher: Matcher<Node>) = object : TypeSafeDiagnosingMatcher<Node>() {
+ override fun matchesSafely(node: Node, mismatchDescription: Description): Boolean {
+ val elementNode = node.childNodes.toList().singleOrNull { it.nodeType == Node.ELEMENT_NODE }
+ if (elementNode == null) {
+ mismatchDescription.appendText("no node found")
+ return false;
+ }
+ if (!nodeMatcher.matches(elementNode)) {
+ mismatchDescription.appendText("contained node was ");
+ nodeMatcher.describeMismatch(elementNode, mismatchDescription)
+ return false
+ }
+ return true
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("node containing ").appendDescriptionOf(nodeMatcher)
+ }
+}
+
+/**
+ * Creates a Hamcrest [matcher][Matcher] that matches HTML <img> nodes.
+ *
+ * @param[url] The source URL of the image tag
+ * @param[title] The title attribute of the image tag
+ * @param[alt] The alt attribute of the image tag
+ * @return A [matcher][Matcher] for [nodes][Node]
+ */
+fun isImageNode(url: String, title: String, alt: String) = object : TypeSafeDiagnosingMatcher<Node>() {
+ override fun matchesSafely(node: Node, mismatchDescription: Description) =
+ if ((node.nodeType != Node.ELEMENT_NODE) || (node.nodeName != "img")) {
+ mismatchDescription.appendText("is not an image node")
+ false
+ } else if (node.attributes.getNamedItem("src").nodeValue != url) {
+ mismatchDescription.appendText("src is ").appendValue(node.attributes.getNamedItem("src").nodeValue)
+ false
+ } else if (node.attributes.getNamedItem("title").nodeValue != title) {
+ mismatchDescription.appendText("title is ").appendValue(node.attributes.getNamedItem("title").nodeValue)
+ false
+ } else if (node.attributes.getNamedItem("alt").nodeValue != alt) {
+ mismatchDescription.appendText("alt is ").appendValue(node.attributes.getNamedItem("alt").nodeValue)
+ false
+ } else {
+ true
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("image node with src ").appendValue(url).appendText(" and title ").appendValue(title).appendText(" and alt text ").appendValue(alt)
+ }
+}