Image loader tests
This commit is contained in:
		
							parent
							
								
									ab4c80dca5
								
							
						
					
					
						commit
						173425ed53
					
				| @ -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()); | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>> visitorMap = Collections.emptyMap(); | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,11 @@ | ||||
| # LaTeX | ||||
| 
 | ||||
| [Documentation](https://noties.github.io/Markwon/docs/ext-latex) | ||||
|  | ||||
|  | ||||
| 
 | ||||
| ```kotlin | ||||
| implementation "io.noties.markwon:ext-strikethrough:${markwonVersion}" | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| [Documentation](https://noties.github.io/Markwon/docs/v3/ext-latex) | ||||
|  | ||||
| @ -1,6 +1,12 @@ | ||||
| # Strikethrough | ||||
| 
 | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20AND%20a%3A%22ext-strikethrough%22) | ||||
|  | ||||
|  | ||||
| 
 | ||||
| ```kotlin | ||||
| implementation "io.noties.markwon:ext-strikethrough:${markwonVersion}" | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: | ||||
| 
 | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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<Throwable> 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<String> 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<Throwable> 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<String> 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<Runnable> 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<String> 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<Runnable> 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<String> supportedSchemes() { | ||||
|                         return Collections.singleton("ftp"); | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final String destination = "ftp://xxx"; | ||||
|         final AsyncDrawable asyncDrawable = asyncDrawable(destination); | ||||
| 
 | ||||
|         impl.load(asyncDrawable); | ||||
| 
 | ||||
|         final ArgumentCaptor<Throwable> 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<Runnable> 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<String> supportedSchemes() { | ||||
|                         return Collections.singleton("ja"); | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final String destination = "ja://jajaja"; | ||||
|         final AsyncDrawable asyncDrawable = asyncDrawable(destination); | ||||
| 
 | ||||
|         impl.load(asyncDrawable); | ||||
| 
 | ||||
|         final ArgumentCaptor<Runnable> 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<String> 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<Runnable> 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<Runnable> 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<String> 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<Future>() { | ||||
|             @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; | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov