From 173425ed536e7fbd545845f86597defb0d48e35c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 5 Jun 2019 15:17:53 +0300 Subject: [PATCH] Image loader tests --- .../markwon/MarkwonBuilderImplTest.java | 1 - .../noties/markwon/core/CorePluginTest.java | 6 +- .../markwon/syntax/SyntaxHighlightTest.java | 3 +- markwon-ext-latex/README.md | 10 +- markwon-ext-strikethrough/README.md | 8 +- markwon-image/build.gradle | 7 +- .../image/AsyncDrawableLoaderBuilder.java | 11 + .../image/AsyncDrawableLoaderImpl.java | 9 +- .../markwon/image/gif/GifMediaDecoder.java | 23 +- .../markwon/image/svg/SvgMediaDecoder.java | 14 +- .../image/AsyncDrawableLoaderBuilderTest.java | 18 +- .../image/AsyncDrawableLoaderImplTest.java | 520 +++++++++++++++++- .../markwon/image/ImagesPluginTest.java | 1 - 13 files changed, 585 insertions(+), 46 deletions(-) diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java index 56b1ee60..84a3a38e 100644 --- a/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java @@ -88,7 +88,6 @@ public class MarkwonBuilderImplTest { verify(plugin, times(1)).configureConfiguration(any(MarkwonConfiguration.Builder.class)); verify(plugin, times(1)).configureVisitor(any(MarkwonVisitor.Builder.class)); verify(plugin, times(1)).configureSpansFactory(any(MarkwonSpansFactory.Builder.class)); - verify(plugin, times(1)).configureHtmlRenderer(any(MarkwonHtmlRenderer.Builder.class)); // note, no render props -> they must be configured on render stage verify(plugin, times(0)).processMarkdown(anyString()); diff --git a/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java b/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java index 3d9834dc..cda6c9b8 100644 --- a/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java @@ -3,6 +3,7 @@ package io.noties.markwon.core; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.method.MovementMethod; +import android.widget.ImageView; import android.widget.TextView; import org.commonmark.node.BlockQuote; @@ -12,6 +13,7 @@ import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; +import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; import org.commonmark.node.ListItem; @@ -84,7 +86,8 @@ public class CorePluginTest { SoftLineBreak.class, StrongEmphasis.class, Text.class, - ThematicBreak.class + ThematicBreak.class, + Image.class }; final CorePlugin plugin = CorePlugin.create(); @@ -202,6 +205,7 @@ public class CorePluginTest { add("beforeSetText"); add("afterSetText"); add("priority"); + add("addOnTextAddedListener"); }}; // we will use declaredMethods because it won't return inherited ones diff --git a/markwon-core/src/test/java/io/noties/markwon/syntax/SyntaxHighlightTest.java b/markwon-core/src/test/java/io/noties/markwon/syntax/SyntaxHighlightTest.java index 3024fcc0..a591303e 100644 --- a/markwon-core/src/test/java/io/noties/markwon/syntax/SyntaxHighlightTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/syntax/SyntaxHighlightTest.java @@ -26,7 +26,6 @@ import io.noties.markwon.SpanFactory; import io.noties.markwon.SpannableBuilder; import io.noties.markwon.core.CorePluginBridge; import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.html.MarkwonHtmlRenderer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -83,7 +82,7 @@ public class SyntaxHighlightTest { final MarkwonConfiguration configuration = MarkwonConfiguration.builder() .syntaxHighlight(highlight) - .build(mock(MarkwonTheme.class), mock(MarkwonHtmlRenderer.class), spansFactory); + .build(mock(MarkwonTheme.class), spansFactory); final Map, MarkwonVisitor.NodeVisitor> visitorMap = Collections.emptyMap(); diff --git a/markwon-ext-latex/README.md b/markwon-ext-latex/README.md index 620b05fa..442fc61e 100644 --- a/markwon-ext-latex/README.md +++ b/markwon-ext-latex/README.md @@ -1,3 +1,11 @@ # LaTeX -[Documentation](https://noties.github.io/Markwon/docs/ext-latex) +![stable](https://img.shields.io/maven-central/v/io.noties.markwon/ext-latex.svg) +![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/ext-latex.svg) + +```kotlin +implementation "io.noties.markwon:ext-strikethrough:${markwonVersion}" +``` + + +[Documentation](https://noties.github.io/Markwon/docs/v3/ext-latex) diff --git a/markwon-ext-strikethrough/README.md b/markwon-ext-strikethrough/README.md index 1f99b846..8ef64b38 100644 --- a/markwon-ext-strikethrough/README.md +++ b/markwon-ext-strikethrough/README.md @@ -1,6 +1,12 @@ # Strikethrough -[![ext-strikethrough](https://img.shields.io/maven-central/v/io.noties.markwon/ext-strikethrough.svg?label=ext-strikethrough)](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20AND%20a%3A%22ext-strikethrough%22) +![stable](https://img.shields.io/maven-central/v/io.noties.markwon/ext-strikethrough.svg) +![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/ext-strikethrough.svg) + +```kotlin +implementation "io.noties.markwon:ext-strikethrough:${markwonVersion}" +``` + This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: diff --git a/markwon-image/build.gradle b/markwon-image/build.gradle index 2e0c86dd..16c70814 100644 --- a/markwon-image/build.gradle +++ b/markwon-image/build.gradle @@ -17,10 +17,11 @@ dependencies { api project(':markwon-core') + // todo: note that it includes these implicitly deps.with { - compileOnly it['android-gif'] - compileOnly it['android-svg'] - compileOnly it['okhttp'] + api it['android-gif'] + api it['android-svg'] + api it['okhttp'] } deps['test'].with { diff --git a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderBuilder.java b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderBuilder.java index 7f8c0dcb..7ee1116e 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderBuilder.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderBuilder.java @@ -9,7 +9,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.noties.markwon.image.data.DataUriSchemeHandler; +import io.noties.markwon.image.gif.GifMediaDecoder; import io.noties.markwon.image.network.NetworkSchemeHandler; +import io.noties.markwon.image.svg.SvgMediaDecoder; class AsyncDrawableLoaderBuilder { @@ -30,6 +32,15 @@ class AsyncDrawableLoaderBuilder { addSchemeHandler(DataUriSchemeHandler.create()); addSchemeHandler(NetworkSchemeHandler.create()); + // add SVG and GIF, but only if they are present in the class-path + if (SvgMediaDecoder.available()) { + addMediaDecoder(SvgMediaDecoder.create()); + } + + if (GifMediaDecoder.available()) { + addMediaDecoder(GifMediaDecoder.create()); + } + defaultMediaDecoder = DefaultImageMediaDecoder.create(); } diff --git a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java index c03529d7..54e6c1b2 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderImpl.java @@ -92,8 +92,15 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { Drawable drawable = null; try { + + final String scheme = uri.getScheme(); + if (scheme == null + || scheme.length() == 0) { + throw new IllegalStateException("No scheme is found: " + destination); + } + // obtain scheme handler - final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); + final SchemeHandler schemeHandler = schemeHandlers.get(scheme); if (schemeHandler != null) { // handle scheme diff --git a/markwon-image/src/main/java/io/noties/markwon/image/gif/GifMediaDecoder.java b/markwon-image/src/main/java/io/noties/markwon/image/gif/GifMediaDecoder.java index d8757df4..508c6667 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/gif/GifMediaDecoder.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/gif/GifMediaDecoder.java @@ -10,9 +10,9 @@ import java.io.InputStream; import java.util.Collection; import java.util.Collections; +import io.noties.markwon.image.DrawableUtils; import io.noties.markwon.image.MediaDecoder; import pl.droidsonroids.gif.GifDrawable; -import io.noties.markwon.image.DrawableUtils; /** * @since 1.1.0 @@ -22,11 +22,29 @@ public class GifMediaDecoder extends MediaDecoder { public static final String CONTENT_TYPE = "image/gif"; + /** + * Creates a {@link GifMediaDecoder} with {@code autoPlayGif = true} + * + * @since 4.0.0-SNAPSHOT + */ + @NonNull + public static GifMediaDecoder create() { + return create(true); + } + @NonNull public static GifMediaDecoder create(boolean autoPlayGif) { return new GifMediaDecoder(autoPlayGif); } + /** + * @return boolean indicating if GIF dependency is satisfied + * @since 4.0.0-SNAPSHOT + */ + public static boolean available() { + return Holder.HAS_GIF; + } + private final boolean autoPlayGif; protected GifMediaDecoder(boolean autoPlayGif) { @@ -105,7 +123,8 @@ public class GifMediaDecoder extends MediaDecoder { static void validate() { if (!HAS_GIF) { throw new IllegalStateException("`pl.droidsonroids.gif:android-gif-drawable:*` " + - "dependency is missing, please add to your project explicitly"); + "dependency is missing, please add to your project explicitly if you " + + "wish to use GIF media decoder"); } } } diff --git a/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgMediaDecoder.java b/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgMediaDecoder.java index 229899c2..7482262e 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgMediaDecoder.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/svg/SvgMediaDecoder.java @@ -15,8 +15,8 @@ import java.io.InputStream; import java.util.Collection; import java.util.Collections; -import io.noties.markwon.image.MediaDecoder; import io.noties.markwon.image.DrawableUtils; +import io.noties.markwon.image.MediaDecoder; /** * @since 1.1.0 @@ -31,7 +31,7 @@ public class SvgMediaDecoder extends MediaDecoder { */ @NonNull public static SvgMediaDecoder create() { - return new SvgMediaDecoder(Resources.getSystem()); + return create(Resources.getSystem()); } @NonNull @@ -39,6 +39,14 @@ public class SvgMediaDecoder extends MediaDecoder { return new SvgMediaDecoder(resources); } + /** + * @return boolean indicating if SVG dependency is satisfied + * @since 4.0.0-SNAPSHOT + */ + public static boolean available() { + return Holder.HAS_SVG; + } + private final Resources resources; @SuppressWarnings("WeakerAccess") @@ -102,7 +110,7 @@ public class SvgMediaDecoder extends MediaDecoder { static void validate() { if (!HAS_SVG) { throw new IllegalStateException("`com.caverock:androidsvg:*` dependency is missing, " + - "please add to your project explicitly"); + "please add to your project explicitly if you wish to use SVG media decoder"); } } } diff --git a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java index e08328d2..51006e38 100644 --- a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java +++ b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java @@ -10,8 +10,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.concurrent.ExecutorService; -import io.noties.markwon.image.network.NetworkSchemeHandler; import io.noties.markwon.image.data.DataUriSchemeHandler; +import io.noties.markwon.image.network.NetworkSchemeHandler; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -62,7 +62,7 @@ public class AsyncDrawableLoaderBuilderTest { public void defaults_initialized() { // default-media-decoder and executor-service must be initialized - assertNull(builder.defaultMediaDecoder); + assertNotNull(builder.defaultMediaDecoder); assertNull(builder.executorService); builder.build(); @@ -71,6 +71,18 @@ public class AsyncDrawableLoaderBuilderTest { assertNotNull(builder.executorService); } + @Test + public void default_media_decoder_removed() { + // we init default-media-decoder right away, but further it can be removed (nulled-out) + + assertNotNull(builder.defaultMediaDecoder); + + builder.defaultMediaDecoder(null); + builder.build(); + + assertNull(builder.defaultMediaDecoder); + } + @Test public void executor() { // supplied executor-service must be used @@ -155,7 +167,7 @@ public class AsyncDrawableLoaderBuilderTest { @Test public void default_media_decoder() { - assertNull(builder.defaultMediaDecoder); + assertNotNull(builder.defaultMediaDecoder); final MediaDecoder mediaDecoder = mock(MediaDecoder.class); builder.defaultMediaDecoder(mediaDecoder); diff --git a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderImplTest.java b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderImplTest.java index 2737fcc6..c2e65dcd 100644 --- a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderImplTest.java +++ b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderImplTest.java @@ -1,5 +1,7 @@ package io.noties.markwon.image; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -7,15 +9,31 @@ import android.support.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import io.noties.markwon.image.ImagesPlugin.ErrorHandler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -40,60 +58,508 @@ public class AsyncDrawableLoaderImplTest { .providePlaceholder(any(AsyncDrawable.class)); } + @Test + public void load_cancel() { + // verify that load/cancel works as expected + + final ExecutorService executorService = mock(ExecutorService.class); + final Future future = mock(Future.class); + { + //noinspection unchecked + when(executorService.submit(any(Runnable.class))) + .thenReturn(future); + } + + final Handler handler = mock(Handler.class); + + final AsyncDrawable drawable = mock(AsyncDrawable.class); + + impl = builder + .executorService(executorService) + .handler(handler) + .build(); + + impl.load(drawable); + + verify(executorService, times(1)).submit(any(Runnable.class)); + + impl.cancel(drawable); + + verify(future, times(1)).cancel(eq(true)); + verify(handler, times(1)).removeCallbacksAndMessages(eq(drawable)); + } + + @Test + public void load_no_scheme_handler() { + // when loading is triggered for a scheme which has no registered scheme-handler + + final ErrorHandler errorHandler = mock(ErrorHandler.class); + + impl = builder + .executorService(immediateExecutorService()) + .errorHandler(errorHandler) + .build(); + + final String destination = "blah://blah.JPEG"; + + impl.load(asyncDrawable(destination)); + + final ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(errorHandler, times(1)) + .handleError(eq(destination), throwableCaptor.capture()); + final Throwable value = throwableCaptor.getValue(); + assertTrue(value.getClass().getName(), value instanceof IllegalStateException); + assertTrue(value.getMessage(), value.getMessage().contains("No scheme-handler is found")); + assertTrue(value.getMessage(), value.getMessage().contains(destination)); + } + + @Test + public void load_scheme_handler_throws() { + + final ErrorHandler errorHandler = mock(ErrorHandler.class); + final SchemeHandler schemeHandler = new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + throw new RuntimeException("We throw!"); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("hey"); + } + }; + + impl = builder + .executorService(immediateExecutorService()) + .errorHandler(errorHandler) + .addSchemeHandler(schemeHandler) + .build(); + + final String destination = "hey://whe.er"; + + impl.load(asyncDrawable(destination)); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(errorHandler, times(1)) + .handleError(eq(destination), captor.capture()); + + final Throwable throwable = captor.getValue(); + assertTrue(throwable.getClass().getName(), throwable instanceof RuntimeException); + assertEquals("We throw!", throwable.getMessage()); + } + + @Test + public void load_scheme_handler_returns_result() { + + final Drawable drawable = mock(Drawable.class); + final SchemeHandler schemeHandler = new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withResult(drawable); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("*"); + } + }; + + final String destination = "*://yo"; + + final Future future = mock(Future.class); + final ExecutorService executorService = immediateExecutorService(future); + final Handler handler = mock(Handler.class); + + impl = builder + .executorService(executorService) + .handler(handler) + .addSchemeHandler(schemeHandler) + .build(); + + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + verify(executorService, times(1)) + .submit(any(Runnable.class)); + + // we must use captor in order to let the internal (async) logic settle + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + captor.getValue().run(); + + verify(asyncDrawable, times(1)) + .setResult(eq(drawable)); + + // now, let's cancel the request (at this point it must be removed from referencing) + impl.cancel(asyncDrawable); + + verify(future, never()).cancel(anyBoolean()); + + // this method will be called anyway (we have no mean to check if token has queue) +// verify(handler, never()).removeCallbacksAndMessages(eq(asyncDrawable)); + } + + @Test + public void load_scheme_handler_returns_decoding_default_used() { + // we won't be registering media decoder, but provide a default one (which must be used) + + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + final InputStream inputStream = mock(InputStream.class); + final Drawable drawable = mock(Drawable.class); + + { + when(mediaDecoder.decode(any(String.class), any(InputStream.class))) + .thenReturn(drawable); + } + + impl = builder + .executorService(immediateExecutorService(mock(Future.class))) + .defaultMediaDecoder(mediaDecoder) + .addSchemeHandler(new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withDecodingNeeded("no/op", inputStream); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("whatever"); + } + }) + .build(); + + final String destination = "whatever://yeah-yeah-yeah"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + verify(mediaDecoder, times(1)) + .decode(eq("no/op"), eq(inputStream)); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(builder._handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + captor.getValue().run(); + + verify(asyncDrawable, times(1)) + .setResult(eq(drawable)); + } + + @Test + public void load_no_media_decoder_present() { + // if some content-type is requested (and it has no registered media-decoder), + // and default-media-decoder is not added -> throws + + final ErrorHandler errorHandler = mock(ErrorHandler.class); + + impl = builder + .defaultMediaDecoder(null) + .executorService(immediateExecutorService()) + .errorHandler(errorHandler) + .addSchemeHandler(new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withDecodingNeeded("np/op", mock(InputStream.class)); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("ftp"); + } + }) + .build(); + + final String destination = "ftp://xxx"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + + verify(errorHandler, times(1)) + .handleError(eq(destination), captor.capture()); + + final Throwable throwable = captor.getValue(); + assertTrue(throwable.getClass().getName(), throwable instanceof IllegalStateException); + assertTrue(throwable.getMessage(), throwable.getMessage().contains("No media-decoder is found")); + assertTrue(throwable.getMessage(), throwable.getMessage().contains(destination)); + } + + @Test + public void load_error_handler_drawable() { + // error-handler can return optional error-drawable that can be used as a result + + final ErrorHandler errorHandler = mock(ErrorHandler.class); + final Drawable drawable = mock(Drawable.class); + { + when(errorHandler.handleError(any(String.class), any(Throwable.class))) + .thenReturn(drawable); + } + + impl = builder + .executorService(immediateExecutorService(mock(Future.class))) + .errorHandler(errorHandler) + .build(); + + // we will rely on _internal_ error, which is also delivered to error-handler + // in this case -> no scheme-handler + + final String destination = "uo://uo?true=false"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + verify(errorHandler, times(1)) + .handleError(eq(destination), any(Throwable.class)); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(builder._handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + captor.getValue().run(); + + verify(asyncDrawable, times(1)) + .setResult(eq(drawable)); + } + + @Test + public void load_success_request_cancelled() { + // when loading finishes it must check if request had been cancelled and not deliver result + + impl = builder + .executorService(immediateExecutorService(mock(Future.class))) + .addSchemeHandler(new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withResult(mock(Drawable.class)); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("ja"); + } + }) + .build(); + + final String destination = "ja://jajaja"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(builder._handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + // now, cancel + impl.cancel(asyncDrawable); + + captor.getValue().run(); + + verify(asyncDrawable, never()) + .setResult(any(Drawable.class)); + } + + @Test + public void load_success_async_drawable_not_attached() { + // when loading finishes, it must check if async-drawable is attached + + impl = builder + .executorService(immediateExecutorService(mock(Future.class))) + .addSchemeHandler(new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withResult(mock(Drawable.class)); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("ha"); + } + }) + .build(); + + final String destination = "ha://hahaha"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + when(asyncDrawable.isAttached()).thenReturn(false); + + impl.load(asyncDrawable); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(builder._handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + captor.getValue().run(); + + verify(asyncDrawable, never()) + .setResult(any(Drawable.class)); + } + + @Test + public void load_success_result_null() { + // if result is null (but no exception) - no result must be delivered + + // we won't be adding scheme-handler, thus causing internal error + // (will have to mock error-handler because for the tests we re-throw errors) + impl = builder + .executorService(immediateExecutorService(mock(Future.class))) + .errorHandler(mock(ErrorHandler.class)) + .build(); + + final String destination = "xa://xaxaxa"; + final AsyncDrawable asyncDrawable = asyncDrawable(destination); + + impl.load(asyncDrawable); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(builder._handler, times(1)) + .postAtTime(captor.capture(), eq(asyncDrawable), anyLong()); + + captor.getValue().run(); + + verify(asyncDrawable, never()) + .setResult(any(Drawable.class)); + } + + @Test + public void media_decoder_is_used() { + + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + + { + when(mediaDecoder.decode(any(String.class), any(InputStream.class))) + .thenReturn(mock(Drawable.class)); + when(mediaDecoder.supportedTypes()) + .thenReturn(Collections.singleton("fa/ke")); + } + + impl = builder.executorService(immediateExecutorService()) + .addSchemeHandler(new SchemeHandler() { + @NonNull + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + return ImageItem.withDecodingNeeded("fa/ke", mock(InputStream.class)); + } + + @NonNull + @Override + public Collection supportedSchemes() { + return Collections.singleton("fake"); + } + }) + .addMediaDecoder(mediaDecoder) + .build(); + + final String destination = "fake://1234"; + + impl.load(asyncDrawable(destination)); + + verify(mediaDecoder, times(1)) + .decode(eq("fa/ke"), any(InputStream.class)); + } + private static class BuilderImpl { - AsyncDrawableLoaderBuilder builder; - Handler handler = mock(Handler.class); + AsyncDrawableLoaderBuilder _builder = new AsyncDrawableLoaderBuilder(); + Handler _handler = mock(Handler.class); - public BuilderImpl executorService(@NonNull ExecutorService executorService) { - builder.executorService(executorService); + { + // be default it just logs the exception, let's rethrow + _builder.errorHandler(new ErrorHandler() { + @Nullable + @Override + public Drawable handleError(@NonNull String url, @NonNull Throwable throwable) { + throw new AsyncDrawableException(url, throwable); + } + }); + } + + BuilderImpl executorService(@NonNull ExecutorService executorService) { + _builder.executorService(executorService); return this; } - public BuilderImpl addSchemeHandler(@NonNull SchemeHandler schemeHandler) { - builder.addSchemeHandler(schemeHandler); + BuilderImpl addSchemeHandler(@NonNull SchemeHandler schemeHandler) { + _builder.addSchemeHandler(schemeHandler); return this; } - public BuilderImpl addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { - builder.addMediaDecoder(mediaDecoder); + BuilderImpl addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { + _builder.addMediaDecoder(mediaDecoder); return this; } - public BuilderImpl defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { - builder.defaultMediaDecoder(mediaDecoder); + BuilderImpl defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { + _builder.defaultMediaDecoder(mediaDecoder); return this; } - public BuilderImpl removeSchemeHandler(@NonNull String scheme) { - builder.removeSchemeHandler(scheme); + BuilderImpl placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) { + _builder.placeholderProvider(placeholderDrawableProvider); return this; } - public BuilderImpl removeMediaDecoder(@NonNull String contentType) { - builder.removeMediaDecoder(contentType); - return this; - } - - public BuilderImpl placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) { - builder.placeholderProvider(placeholderDrawableProvider); - return this; - } - - public BuilderImpl errorHandler(@NonNull ImagesPlugin.ErrorHandler errorHandler) { - builder.errorHandler(errorHandler); + BuilderImpl errorHandler(@NonNull ErrorHandler errorHandler) { + _builder.errorHandler(errorHandler); return this; } @NonNull - public BuilderImpl handler(Handler handler) { - this.handler = handler; + BuilderImpl handler(Handler handler) { + this._handler = handler; return this; } @NonNull AsyncDrawableLoaderImpl build() { - return new AsyncDrawableLoaderImpl(builder, handler); + return new AsyncDrawableLoaderImpl(_builder, _handler); + } + + private static class AsyncDrawableException extends RuntimeException { + AsyncDrawableException(String message, Throwable cause) { + super(message, cause); + } } } + + @NonNull + private static ExecutorService immediateExecutorService() { + return immediateExecutorService(null); + } + + @NonNull + private static ExecutorService immediateExecutorService(@Nullable final Future future) { + final ExecutorService service = mock(ExecutorService.class); + when(service.submit(any(Runnable.class))).then(new Answer() { + @Override + public Future answer(InvocationOnMock invocation) { + ((Runnable) invocation.getArgument(0)).run(); + return future; + } + }); + return service; + } + + @NonNull + private static AsyncDrawable asyncDrawable(@NonNull String destination) { + final AsyncDrawable drawable = mock(AsyncDrawable.class); + when(drawable.getDestination()).thenReturn(destination); + when(drawable.isAttached()).thenReturn(true); + return drawable; + } } \ No newline at end of file diff --git a/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java b/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java index 20e7feb5..27f2fbb9 100644 --- a/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java +++ b/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java @@ -18,7 +18,6 @@ import java.util.concurrent.ExecutorService; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.SpanFactory; -import ru.noties.markwon.image.R; import io.noties.markwon.image.data.DataUriSchemeHandler; import static org.junit.Assert.assertNotNull;