Image loader tests

This commit is contained in:
Dimitry Ivanov 2019-06-05 15:17:53 +03:00
parent ab4c80dca5
commit 173425ed53
13 changed files with 585 additions and 46 deletions

View File

@ -88,7 +88,6 @@ public class MarkwonBuilderImplTest {
verify(plugin, times(1)).configureConfiguration(any(MarkwonConfiguration.Builder.class)); verify(plugin, times(1)).configureConfiguration(any(MarkwonConfiguration.Builder.class));
verify(plugin, times(1)).configureVisitor(any(MarkwonVisitor.Builder.class)); verify(plugin, times(1)).configureVisitor(any(MarkwonVisitor.Builder.class));
verify(plugin, times(1)).configureSpansFactory(any(MarkwonSpansFactory.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 // note, no render props -> they must be configured on render stage
verify(plugin, times(0)).processMarkdown(anyString()); verify(plugin, times(0)).processMarkdown(anyString());

View File

@ -3,6 +3,7 @@ package io.noties.markwon.core;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.method.MovementMethod; import android.text.method.MovementMethod;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.node.BlockQuote; import org.commonmark.node.BlockQuote;
@ -12,6 +13,7 @@ import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak; import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading; import org.commonmark.node.Heading;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link; import org.commonmark.node.Link;
import org.commonmark.node.ListItem; import org.commonmark.node.ListItem;
@ -84,7 +86,8 @@ public class CorePluginTest {
SoftLineBreak.class, SoftLineBreak.class,
StrongEmphasis.class, StrongEmphasis.class,
Text.class, Text.class,
ThematicBreak.class ThematicBreak.class,
Image.class
}; };
final CorePlugin plugin = CorePlugin.create(); final CorePlugin plugin = CorePlugin.create();
@ -202,6 +205,7 @@ public class CorePluginTest {
add("beforeSetText"); add("beforeSetText");
add("afterSetText"); add("afterSetText");
add("priority"); add("priority");
add("addOnTextAddedListener");
}}; }};
// we will use declaredMethods because it won't return inherited ones // we will use declaredMethods because it won't return inherited ones

View File

@ -26,7 +26,6 @@ import io.noties.markwon.SpanFactory;
import io.noties.markwon.SpannableBuilder; import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.core.CorePluginBridge; import io.noties.markwon.core.CorePluginBridge;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -83,7 +82,7 @@ public class SyntaxHighlightTest {
final MarkwonConfiguration configuration = MarkwonConfiguration.builder() final MarkwonConfiguration configuration = MarkwonConfiguration.builder()
.syntaxHighlight(highlight) .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(); final Map<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>> visitorMap = Collections.emptyMap();

View File

@ -1,3 +1,11 @@
# LaTeX # 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)

View File

@ -1,6 +1,12 @@
# Strikethrough # 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`: This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`:

View File

@ -17,10 +17,11 @@ dependencies {
api project(':markwon-core') api project(':markwon-core')
// todo: note that it includes these implicitly
deps.with { deps.with {
compileOnly it['android-gif'] api it['android-gif']
compileOnly it['android-svg'] api it['android-svg']
compileOnly it['okhttp'] api it['okhttp']
} }
deps['test'].with { deps['test'].with {

View File

@ -9,7 +9,9 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import io.noties.markwon.image.data.DataUriSchemeHandler; 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.network.NetworkSchemeHandler;
import io.noties.markwon.image.svg.SvgMediaDecoder;
class AsyncDrawableLoaderBuilder { class AsyncDrawableLoaderBuilder {
@ -30,6 +32,15 @@ class AsyncDrawableLoaderBuilder {
addSchemeHandler(DataUriSchemeHandler.create()); addSchemeHandler(DataUriSchemeHandler.create());
addSchemeHandler(NetworkSchemeHandler.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(); defaultMediaDecoder = DefaultImageMediaDecoder.create();
} }

View File

@ -92,8 +92,15 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
Drawable drawable = null; Drawable drawable = null;
try { try {
final String scheme = uri.getScheme();
if (scheme == null
|| scheme.length() == 0) {
throw new IllegalStateException("No scheme is found: " + destination);
}
// obtain scheme handler // obtain scheme handler
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); final SchemeHandler schemeHandler = schemeHandlers.get(scheme);
if (schemeHandler != null) { if (schemeHandler != null) {
// handle scheme // handle scheme

View File

@ -10,9 +10,9 @@ import java.io.InputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import io.noties.markwon.image.DrawableUtils;
import io.noties.markwon.image.MediaDecoder; import io.noties.markwon.image.MediaDecoder;
import pl.droidsonroids.gif.GifDrawable; import pl.droidsonroids.gif.GifDrawable;
import io.noties.markwon.image.DrawableUtils;
/** /**
* @since 1.1.0 * @since 1.1.0
@ -22,11 +22,29 @@ public class GifMediaDecoder extends MediaDecoder {
public static final String CONTENT_TYPE = "image/gif"; 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 @NonNull
public static GifMediaDecoder create(boolean autoPlayGif) { public static GifMediaDecoder create(boolean autoPlayGif) {
return new GifMediaDecoder(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; private final boolean autoPlayGif;
protected GifMediaDecoder(boolean autoPlayGif) { protected GifMediaDecoder(boolean autoPlayGif) {
@ -105,7 +123,8 @@ public class GifMediaDecoder extends MediaDecoder {
static void validate() { static void validate() {
if (!HAS_GIF) { if (!HAS_GIF) {
throw new IllegalStateException("`pl.droidsonroids.gif:android-gif-drawable:*` " + 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");
} }
} }
} }

View File

@ -15,8 +15,8 @@ import java.io.InputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import io.noties.markwon.image.MediaDecoder;
import io.noties.markwon.image.DrawableUtils; import io.noties.markwon.image.DrawableUtils;
import io.noties.markwon.image.MediaDecoder;
/** /**
* @since 1.1.0 * @since 1.1.0
@ -31,7 +31,7 @@ public class SvgMediaDecoder extends MediaDecoder {
*/ */
@NonNull @NonNull
public static SvgMediaDecoder create() { public static SvgMediaDecoder create() {
return new SvgMediaDecoder(Resources.getSystem()); return create(Resources.getSystem());
} }
@NonNull @NonNull
@ -39,6 +39,14 @@ public class SvgMediaDecoder extends MediaDecoder {
return new SvgMediaDecoder(resources); 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; private final Resources resources;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@ -102,7 +110,7 @@ public class SvgMediaDecoder extends MediaDecoder {
static void validate() { static void validate() {
if (!HAS_SVG) { if (!HAS_SVG) {
throw new IllegalStateException("`com.caverock:androidsvg:*` dependency is missing, " + 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");
} }
} }
} }

View File

@ -10,8 +10,8 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import io.noties.markwon.image.network.NetworkSchemeHandler;
import io.noties.markwon.image.data.DataUriSchemeHandler; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -62,7 +62,7 @@ public class AsyncDrawableLoaderBuilderTest {
public void defaults_initialized() { public void defaults_initialized() {
// default-media-decoder and executor-service must be initialized // default-media-decoder and executor-service must be initialized
assertNull(builder.defaultMediaDecoder); assertNotNull(builder.defaultMediaDecoder);
assertNull(builder.executorService); assertNull(builder.executorService);
builder.build(); builder.build();
@ -71,6 +71,18 @@ public class AsyncDrawableLoaderBuilderTest {
assertNotNull(builder.executorService); 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 @Test
public void executor() { public void executor() {
// supplied executor-service must be used // supplied executor-service must be used
@ -155,7 +167,7 @@ public class AsyncDrawableLoaderBuilderTest {
@Test @Test
public void default_media_decoder() { public void default_media_decoder() {
assertNull(builder.defaultMediaDecoder); assertNotNull(builder.defaultMediaDecoder);
final MediaDecoder mediaDecoder = mock(MediaDecoder.class); final MediaDecoder mediaDecoder = mock(MediaDecoder.class);
builder.defaultMediaDecoder(mediaDecoder); builder.defaultMediaDecoder(mediaDecoder);

View File

@ -1,5 +1,7 @@
package io.noties.markwon.image; package io.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -7,15 +9,31 @@ import android.support.annotation.Nullable;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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.RobolectricTestRunner;
import org.robolectric.annotation.Config; 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.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.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.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE) @Config(manifest = Config.NONE)
@ -40,60 +58,508 @@ public class AsyncDrawableLoaderImplTest {
.providePlaceholder(any(AsyncDrawable.class)); .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 { private static class BuilderImpl {
AsyncDrawableLoaderBuilder builder; AsyncDrawableLoaderBuilder _builder = new AsyncDrawableLoaderBuilder();
Handler handler = mock(Handler.class); 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; return this;
} }
public BuilderImpl addSchemeHandler(@NonNull SchemeHandler schemeHandler) { BuilderImpl addSchemeHandler(@NonNull SchemeHandler schemeHandler) {
builder.addSchemeHandler(schemeHandler); _builder.addSchemeHandler(schemeHandler);
return this; return this;
} }
public BuilderImpl addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { BuilderImpl addMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
builder.addMediaDecoder(mediaDecoder); _builder.addMediaDecoder(mediaDecoder);
return this; return this;
} }
public BuilderImpl defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { BuilderImpl defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
builder.defaultMediaDecoder(mediaDecoder); _builder.defaultMediaDecoder(mediaDecoder);
return this; return this;
} }
public BuilderImpl removeSchemeHandler(@NonNull String scheme) { BuilderImpl placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) {
builder.removeSchemeHandler(scheme); _builder.placeholderProvider(placeholderDrawableProvider);
return this; return this;
} }
public BuilderImpl removeMediaDecoder(@NonNull String contentType) { BuilderImpl errorHandler(@NonNull ErrorHandler errorHandler) {
builder.removeMediaDecoder(contentType); _builder.errorHandler(errorHandler);
return this;
}
public BuilderImpl placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) {
builder.placeholderProvider(placeholderDrawableProvider);
return this;
}
public BuilderImpl errorHandler(@NonNull ImagesPlugin.ErrorHandler errorHandler) {
builder.errorHandler(errorHandler);
return this; return this;
} }
@NonNull @NonNull
public BuilderImpl handler(Handler handler) { BuilderImpl handler(Handler handler) {
this.handler = handler; this._handler = handler;
return this; return this;
} }
@NonNull @NonNull
AsyncDrawableLoaderImpl build() { 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;
}
} }

View File

@ -18,7 +18,6 @@ import java.util.concurrent.ExecutorService;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.SpanFactory; import io.noties.markwon.SpanFactory;
import ru.noties.markwon.image.R;
import io.noties.markwon.image.data.DataUriSchemeHandler; import io.noties.markwon.image.data.DataUriSchemeHandler;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;