From 675710fc669a9f5ccfab42296a3aa0b822539e14 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 13 Nov 2016 08:09:47 +0100 Subject: [PATCH] Show loading animation while loading elements --- .../net/pterodactylus/sone/web/WebInterface.java | 10 ++- .../sone/core/DefaultElementLoader.kt | 56 ++++++++++++++ .../pterodactylus/sone/core/DefaultImageLoader.kt | 39 ---------- .../net/pterodactylus/sone/core/ElementLoader.kt | 22 ++++++ .../net/pterodactylus/sone/core/ImageLoader.kt | 15 ---- .../sone/template/LinkedElementRenderFilter.kt | 47 ++++++++++++ .../sone/template/LinkedElementsFilter.kt | 23 ++++++ .../sone/template/LinkedImagesFilter.kt | 23 ------ src/main/resources/static/css/sone.css | 8 +- .../resources/static/images/loading-animation.gif | Bin 0 -> 72756 bytes src/main/resources/templates/include/viewPost.html | 10 +-- .../resources/templates/include/viewReply.html | 10 +-- .../sone/core/DefaultElementLoaderTest.kt | 81 +++++++++++++++++++++ .../sone/core/DefaultImageLoaderTest.kt | 59 --------------- .../pterodactylus/sone/core/ElementLoaderTest.kt | 20 +++++ .../net/pterodactylus/sone/core/ImageLoaderTest.kt | 20 ----- .../sone/template/LinkedElementRenderFilterTest.kt | 37 ++++++++++ .../sone/template/LinkedElementsFilterTest.kt | 40 ++++++++++ .../sone/template/LinkedImagesFilterTest.kt | 37 ---------- 19 files changed, 344 insertions(+), 213 deletions(-) create mode 100644 src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt delete mode 100644 src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt create mode 100644 src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt delete mode 100644 src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt create mode 100644 src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt create mode 100644 src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt delete mode 100644 src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt create mode 100644 src/main/resources/static/images/loading-animation.gif create mode 100644 src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt delete mode 100644 src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt create mode 100644 src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt delete mode 100644 src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt create mode 100644 src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt create mode 100644 src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt delete mode 100644 src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 046a377..50d3323 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -40,7 +40,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.core.ImageLoader; +import net.pterodactylus.sone.core.ElementLoader; import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; import net.pterodactylus.sone.core.event.ImageInsertFailedEvent; import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent; @@ -85,7 +85,8 @@ import net.pterodactylus.sone.template.IdentityAccessor; import net.pterodactylus.sone.template.ImageAccessor; import net.pterodactylus.sone.template.ImageLinkFilter; import net.pterodactylus.sone.template.JavascriptFilter; -import net.pterodactylus.sone.template.LinkedImagesFilter; +import net.pterodactylus.sone.template.LinkedElementRenderFilter; +import net.pterodactylus.sone.template.LinkedElementsFilter; import net.pterodactylus.sone.template.ParserFilter; import net.pterodactylus.sone.template.PostAccessor; import net.pterodactylus.sone.template.ProfileAccessor; @@ -258,7 +259,7 @@ public class WebInterface { * The Sone plugin */ @Inject - public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ImageLoader imageLoader) { + public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ElementLoader elementLoader) { this.sonePlugin = sonePlugin; this.loaders = loaders; this.listNotificationFilter = listNotificationFilter; @@ -293,7 +294,8 @@ public class WebInterface { templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), soneTextParser)); templateContextFactory.addFilter("shorten", shortenFilter = new ShortenFilter()); templateContextFactory.addFilter("render", renderFilter = new RenderFilter(getCore(), templateContextFactory)); - templateContextFactory.addFilter("linked-images", new LinkedImagesFilter(imageLoader)); + templateContextFactory.addFilter("linked-elements", new LinkedElementsFilter(elementLoader)); + templateContextFactory.addFilter("render-linked-element", new LinkedElementRenderFilter(templateContextFactory)); templateContextFactory.addFilter("reparse", new ReparseFilter()); templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate")); templateContextFactory.addFilter("format", new FormatFilter()); diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt new file mode 100644 index 0000000..eabfed8 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt @@ -0,0 +1,56 @@ +package net.pterodactylus.sone.core + +import com.google.common.cache.CacheBuilder +import freenet.keys.FreenetURI +import java.io.ByteArrayInputStream +import javax.imageio.ImageIO +import javax.inject.Inject + +/** + * [ElementLoader] implementation that uses a simple Guava [com.google.common.cache.Cache]. + */ +class DefaultElementLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ElementLoader { + + private val loadingLinks = CacheBuilder.newBuilder().build() + private val imageCache = CacheBuilder.newBuilder().build() + private val callback = object : FreenetInterface.BackgroundFetchCallback { + override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) { + if (!mimeType.startsWith("image/")) { + return + } + ByteArrayInputStream(data).use { + ImageIO.read(it) + }?.let { + imageCache.get(uri.toString()) { LinkedImage(uri.toString()) } + } + removeLoadingLink(uri) + } + + override fun failed(uri: FreenetURI) { + removeLoadingLink(uri) + } + + private fun removeLoadingLink(uri: FreenetURI) { + synchronized(loadingLinks) { + loadingLinks.invalidate(uri.toString()) + } + } + } + + override fun loadElement(link: String): LinkedElement { + synchronized(loadingLinks) { + imageCache.getIfPresent(link)?.run { + return this + } + if (loadingLinks.getIfPresent(link) == null) { + loadingLinks.put(link, true) + freenetInterface.startFetch(FreenetURI(link), callback) + } + } + return object : LinkedElement { + override val link = link + override val loading = true + } + } + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt deleted file mode 100644 index 01e177a..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.pterodactylus.sone.core - -import com.google.common.cache.CacheBuilder -import freenet.keys.FreenetURI -import java.io.ByteArrayInputStream -import javax.imageio.ImageIO -import javax.inject.Inject - -/** - * [ImageLoader] implementation that uses a simple Guava [com.google.common.cache.Cache]. - */ -class DefaultImageLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ImageLoader { - - private val imageCache = CacheBuilder.newBuilder().build() - private val callback = object : FreenetInterface.BackgroundFetchCallback { - override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) { - if (!mimeType.startsWith("image/")) { - return - } - val image = ByteArrayInputStream(data).use { - ImageIO.read(it) - } - val loadedImage = LoadedImage(uri.toString(), mimeType, image.width, image.height) - imageCache.get(uri.toString()) { loadedImage } - } - - override fun failed(uri: FreenetURI) { - } - } - - override fun toLoadedImage(link: String): LoadedImage? { - imageCache.getIfPresent(link)?.run { - return this - } - freenetInterface.startFetch(FreenetURI(link), callback) - return null - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt new file mode 100644 index 0000000..2424cf1 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt @@ -0,0 +1,22 @@ +package net.pterodactylus.sone.core + +import com.google.inject.ImplementedBy + +/** + * Component that loads images and supplies information about them. + */ +@ImplementedBy(DefaultElementLoader::class) +interface ElementLoader { + + fun loadElement(link: String): LinkedElement + +} + +interface LinkedElement { + + val link: String + val loading: Boolean + +} + +data class LinkedImage(override val link: String, override val loading: Boolean = false) : LinkedElement diff --git a/src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt deleted file mode 100644 index 5211015..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.pterodactylus.sone.core - -import com.google.inject.ImplementedBy - -/** - * Component that loads images and supplies information about them. - */ -@ImplementedBy(DefaultImageLoader::class) -interface ImageLoader { - - fun toLoadedImage(link: String): LoadedImage? - -} - -data class LoadedImage(val link: String, val mimeType: String, val width: Int, val height: Int) diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt new file mode 100644 index 0000000..6255f9d --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt @@ -0,0 +1,47 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.LinkedElement +import net.pterodactylus.sone.core.LinkedImage +import net.pterodactylus.util.template.Filter +import net.pterodactylus.util.template.TemplateContext +import net.pterodactylus.util.template.TemplateContextFactory +import net.pterodactylus.util.template.TemplateParser +import java.io.StringReader +import java.io.StringWriter + +/** + * Renders all kinds of [LinkedElement]s. + */ +class LinkedElementRenderFilter(private val templateContextFactory: TemplateContextFactory) : Filter { + + companion object { + private val loadedImageTemplate = """""".parse() + private val notLoadedImageTemplate = """""".parse() + + private fun String.parse() = StringReader(this).use { TemplateParser.parse(it) } + } + + override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map?) = + when { + data is LinkedElement && data.loading -> renderNotLoadedLinkedElement(data) + data is LinkedImage -> renderLinkedImage(data) + else -> null + } + + private fun renderLinkedImage(linkedImage: LinkedImage) = + StringWriter().use { + val templateContext = templateContextFactory.createTemplateContext() + templateContext["link"] = linkedImage.link + loadedImageTemplate.render(templateContext, it) + it + }.toString() + + private fun renderNotLoadedLinkedElement(linkedElement: LinkedElement) = + StringWriter().use { + val templateContext = templateContextFactory.createTemplateContext() + templateContext["link"] = linkedElement.link + notLoadedImageTemplate.render(templateContext, it) + it + }.toString() + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt new file mode 100644 index 0000000..f95d6c0 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt @@ -0,0 +1,23 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.ElementLoader +import net.pterodactylus.sone.core.LinkedElement +import net.pterodactylus.sone.text.FreenetLinkPart +import net.pterodactylus.sone.text.Part +import net.pterodactylus.util.template.Filter +import net.pterodactylus.util.template.TemplateContext + +/** + * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet elements + * with [LinkedElement]s. + */ +class LinkedElementsFilter(private val elementLoader: ElementLoader) : Filter { + + @Suppress("UNCHECKED_CAST") + override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap?) = + (data as? Iterable) + ?.filterIsInstance() + ?.map { elementLoader.loadElement(it.link) } + ?: listOf() + +} diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt deleted file mode 100644 index 584bec3..0000000 --- a/src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.pterodactylus.sone.template - -import net.pterodactylus.sone.core.ImageLoader -import net.pterodactylus.sone.core.LoadedImage -import net.pterodactylus.sone.text.FreenetLinkPart -import net.pterodactylus.sone.text.Part -import net.pterodactylus.util.template.Filter -import net.pterodactylus.util.template.TemplateContext - -/** - * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet images - * with [LoadedImage]s. - */ -class LinkedImagesFilter(private val imageLoader: ImageLoader) : Filter { - - @Suppress("UNCHECKED_CAST") - override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap?) = - (data as? Iterable) - ?.filterIsInstance() - ?.mapNotNull { imageLoader.toLoadedImage(it.link) } - ?: listOf() - -} diff --git a/src/main/resources/static/css/sone.css b/src/main/resources/static/css/sone.css index 8e008c7..34dad50 100644 --- a/src/main/resources/static/css/sone.css +++ b/src/main/resources/static/css/sone.css @@ -440,11 +440,11 @@ textarea { color: green; } -#sone .post .linked-images { +#sone .post .linked-elements { margin-top: 1ex; } -#sone .post .linked-image { +#sone .post .linked-element { display: inline-block; border: solid 1px black; width: 160px; @@ -511,11 +511,11 @@ textarea { font-size: inherit; } -#sone .post .reply .linked-images { +#sone .post .reply .linked-elements { margin-top: 1ex; } -#sone .post .reply .linked-image { +#sone .post .reply .linked-element { display: inline-block; border: solid 1px black; width: 120px; diff --git a/src/main/resources/static/images/loading-animation.gif b/src/main/resources/static/images/loading-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..8d4f3f49654615e19b4b4ed384456048b8719ee6 GIT binary patch literal 72756 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 shortText> <%if !shortText|match value=renderedText><%if !raw><%= View.Post.ShowMore|l10n|html><%/if><%/if> <%if !shortText|match value=renderedText><%if !raw><%/if><%/if> - <% parsedText|linked-images|store key==linkedImages> - <% foreach linkedImages linkedImage> + <% parsedText|linked-elements|store key==linkedElements> + <% foreach linkedElements linkedElement> <% first> -
+
<%/first> - - - + <% linkedElement|render-linked-element> <% last>
<%/last> diff --git a/src/main/resources/templates/include/viewReply.html b/src/main/resources/templates/include/viewReply.html index cd56e32..6091d40 100644 --- a/src/main/resources/templates/include/viewReply.html +++ b/src/main/resources/templates/include/viewReply.html @@ -23,14 +23,12 @@
<% shortText>
<%if !shortText|match value=renderedText><%if !raw><%= View.Post.ShowMore|l10n|html><%/if><%/if> <%if !shortText|match value=renderedText><%if !raw><%/if><%/if> - <% parsedText|linked-images|store key==linkedImages> - <% foreach linkedImages linkedImage> + <% parsedText|linked-elements|store key==linkedElements> + <% foreach linkedElements linkedElement> <% first> -
+
<%/first> - - - + <% linkedElement|render-linked-element> <% last>
<%/last> diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt new file mode 100644 index 0000000..adc5bc9 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt @@ -0,0 +1,81 @@ +package net.pterodactylus.sone.core + +import com.google.common.io.ByteStreams +import com.google.common.io.Files +import freenet.keys.FreenetURI +import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback +import net.pterodactylus.sone.test.capture +import net.pterodactylus.sone.test.mock +import org.hamcrest.MatcherAssert +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.nullValue +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.io.ByteArrayOutputStream + +/** + * Unit test for [DefaultElementLoaderTest]. + */ +class DefaultElementLoaderTest { + + companion object { + private const val IMAGE_ID = "KSK@gpl.png" + } + + private val freenetInterface = mock() + private val elementLoader = DefaultElementLoader(freenetInterface) + private val callback = capture() + + @Test + fun `image loader starts request for link that is not known`() { + elementLoader.loadElement(IMAGE_ID) + verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any()) + } + + @Test + fun `element loader only starts request once`() { + elementLoader.loadElement(IMAGE_ID) + elementLoader.loadElement(IMAGE_ID) + verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any()) + } + + @Test + fun `element loader returns loading element on first call`() { + assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true)) + } + + @Test + fun `image loader can load image`() { + elementLoader.loadElement(IMAGE_ID) + verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture()) + callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png")) + val linkedElement = elementLoader.loadElement(IMAGE_ID) + assertThat(linkedElement.link, `is`(IMAGE_ID)) + assertThat(linkedElement.loading, `is`(false)) + assertThat(linkedElement, instanceOf(LinkedImage::class.java)) + } + + @Test + fun `image can be loaded again after it failed`() { + elementLoader.loadElement(IMAGE_ID) + verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture()) + callback.value.failed(FreenetURI(IMAGE_ID)) + elementLoader.loadElement(IMAGE_ID) + verify(freenetInterface, times(2)).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture()) + } + + private fun read(resource: String): ByteArray = + javaClass.getResourceAsStream(resource)?.use { input -> + ByteArrayOutputStream().use { + ByteStreams.copy(input, it) + it + }.toByteArray() + } ?: ByteArray(0) + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt deleted file mode 100644 index cc31665..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.pterodactylus.sone.core - -import com.google.common.io.ByteStreams -import com.google.common.io.Files -import freenet.keys.FreenetURI -import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback -import net.pterodactylus.sone.test.capture -import net.pterodactylus.sone.test.mock -import org.hamcrest.MatcherAssert -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers -import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.nullValue -import org.junit.Test -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.verify -import java.io.ByteArrayOutputStream - -/** - * Unit test for [DefaultImageLoaderTest]. - */ -class DefaultImageLoaderTest { - - companion object { - private const val IMAGE_ID = "KSK@gpl.png" - } - - private val freenetInterface = mock() - private val imageLoader = DefaultImageLoader(freenetInterface) - private val callback = capture() - - @Test - fun `image loader starts request for link that is not known`() { - assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue()) - verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any()) - } - - @Test - fun `image loader can load image`() { - assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue()) - verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture()) - callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png")) - val loadedImage = imageLoader.toLoadedImage(IMAGE_ID)!! - assertThat(loadedImage.link, `is`(IMAGE_ID)) - assertThat(loadedImage.mimeType, `is`("image/png")) - assertThat(loadedImage.width, `is`(200)) - assertThat(loadedImage.height, `is`(150)) - } - - private fun read(resource: String): ByteArray = - javaClass.getResourceAsStream(resource)?.use { input -> - ByteArrayOutputStream().use { - ByteStreams.copy(input, it) - it - }.toByteArray() - } ?: ByteArray(0) - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt new file mode 100644 index 0000000..dbb5bcc --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt @@ -0,0 +1,20 @@ +package net.pterodactylus.sone.core + +import com.google.inject.Guice.createInjector +import net.pterodactylus.sone.test.bindMock +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.notNullValue +import org.junit.Test + +/** + * Unit test for [ElementLoader]. + */ +class ElementLoaderTest { + + @Test + fun `default image loader can be loaded by guice`() { + val injector = createInjector(bindMock()) + assertThat(injector.getInstance(ElementLoader::class.java), notNullValue()); + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt deleted file mode 100644 index d3a021c..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.pterodactylus.sone.core - -import com.google.inject.Guice.createInjector -import net.pterodactylus.sone.test.bindMock -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.notNullValue -import org.junit.Test - -/** - * Unit test for [ImageLoader]. - */ -class ImageLoaderTest { - - @Test - fun `default image loader can be loaded by guice`() { - val injector = createInjector(bindMock()) - assertThat(injector.getInstance(ImageLoader::class.java), notNullValue()); - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt new file mode 100644 index 0000000..20ee99a --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt @@ -0,0 +1,37 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.LinkedImage +import net.pterodactylus.util.template.HtmlFilter +import net.pterodactylus.util.template.TemplateContextFactory +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.jsoup.Jsoup +import org.junit.Test + +/** + * Unit test for [LinkedElementRenderFilter]. + */ +class LinkedElementRenderFilterTest { + + private val templateContextFactory = TemplateContextFactory() + + init { + templateContextFactory.addFilter("html", HtmlFilter()) + } + + private val filter = LinkedElementRenderFilter(templateContextFactory) + + @Test + fun `filter can render linked images`() { + val html = filter.format(null, LinkedImage("KSK@gpl.png"), emptyMap()) as String + val linkNode = Jsoup.parseBodyFragment(html).body().child(0) + assertThat(linkNode.nodeName(), `is`("a")) + assertThat(linkNode.attr("href"), `is`("/KSK@gpl.png")) + val spanNode = linkNode.child(0) + assertThat(spanNode.nodeName(), `is`("span")) + assertThat(spanNode.attr("class"), `is`("linked-element")) + assertThat(spanNode.attr("title"), `is`("KSK@gpl.png")) + assertThat(spanNode.attr("style"), `is`("background-image: url('/KSK@gpl.png')")) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt new file mode 100644 index 0000000..d519e41 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt @@ -0,0 +1,40 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.ElementLoader +import net.pterodactylus.sone.core.LinkedElement +import net.pterodactylus.sone.core.LinkedImage +import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.text.FreenetLinkPart +import net.pterodactylus.sone.text.LinkPart +import net.pterodactylus.sone.text.PlainTextPart +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.junit.Test +import org.mockito.Mockito.`when` + +/** + * Unit test for [LinkedElementsFilter]. + */ +class LinkedElementsFilterTest { + + private val imageLoader = mock() + private val filter = LinkedElementsFilter(imageLoader) + + @Test + fun `filter finds all loaded freenet images`() { + val parts = listOf( + PlainTextPart("text"), + LinkPart("http://link", "link"), + FreenetLinkPart("KSK@link", "link", false), + FreenetLinkPart("KSK@link.png", "link", false) + ) + `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedImage("KSK@link", true)) + `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedImage("KSK@link.png")) + val loadedImages = filter.format(null, parts, null) + assertThat(loadedImages, contains( + LinkedImage("KSK@link", true), + LinkedImage("KSK@link.png") + )) + } + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt deleted file mode 100644 index 8e86bd4..0000000 --- a/src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.pterodactylus.sone.template - -import net.pterodactylus.sone.core.ImageLoader -import net.pterodactylus.sone.core.LoadedImage -import net.pterodactylus.sone.test.mock -import net.pterodactylus.sone.text.FreenetLinkPart -import net.pterodactylus.sone.text.LinkPart -import net.pterodactylus.sone.text.PlainTextPart -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers -import org.junit.Test -import org.mockito.Mockito.`when` - -/** - * Unit test for [LinkedImagesFilter]. - */ -class LinkedImagesFilterTest { - - private val imageLoader = mock() - private val filter = LinkedImagesFilter(imageLoader) - - @Test - fun `filter finds all loaded freenet images`() { - val parts = listOf( - PlainTextPart("text"), - LinkPart("http://link", "link"), - FreenetLinkPart("KSK@link", "link", false), - FreenetLinkPart("KSK@link.png", "link", false) - ) - `when`(imageLoader.toLoadedImage("KSK@link.png")).thenReturn(LoadedImage("KSK@link.png", "image/png", 1440, 900)) - val loadedImages = filter.format(null, parts, null) - assertThat(loadedImages, Matchers.contains( - LoadedImage("KSK@link.png", "image/png", 1440, 900) - )) - } - -} -- 2.7.4