Add tests for markwon-image module

This commit is contained in:
Dimitry Ivanov 2019-06-03 15:28:52 +03:00
parent cedb3971a0
commit 6bf04e38ad
10 changed files with 425 additions and 16 deletions

View File

@ -165,16 +165,19 @@ public class CorePlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) {
final int length = visitor.length();
final String literal = text.getLiteral(); final String literal = text.getLiteral();
visitor.builder().append(literal); visitor.builder().append(literal);
// @since 4.0.0-SNAPSHOT // @since 4.0.0-SNAPSHOT
if (!onTextAddedListeners.isEmpty()) {
// calculate the start position
final int length = visitor.length() - literal.length();
for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) { for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) {
onTextAddedListener.onTextAdded(visitor, literal, length); onTextAddedListener.onTextAdded(visitor, literal, length);
} }
} }
}
}); });
} }

View File

@ -27,6 +27,4 @@ public abstract class AsyncDrawableLoader {
@Nullable @Nullable
public abstract Drawable placeholder(@NonNull AsyncDrawable drawable); public abstract Drawable placeholder(@NonNull AsyncDrawable drawable);
} }

View File

@ -4,7 +4,7 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
@Override @Override
public void load(@NonNull AsyncDrawable drawable) { public void load(@NonNull AsyncDrawable drawable) {

View File

@ -93,6 +93,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader; private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader;
@SuppressWarnings("WeakerAccess")
JLatexMathPlugin(@NonNull Config config) { JLatexMathPlugin(@NonNull Config config) {
this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config); this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config);
} }

View File

@ -8,6 +8,9 @@ import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import ru.noties.markwon.image.data.DataUriSchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler;
class AsyncDrawableLoaderBuilder { class AsyncDrawableLoaderBuilder {
ExecutorService executorService; ExecutorService executorService;
@ -19,6 +22,15 @@ class AsyncDrawableLoaderBuilder {
boolean isBuilt; boolean isBuilt;
AsyncDrawableLoaderBuilder() {
// @since 4.0.0-SNAPSHOT
// okay, let's add supported schemes at the start, this would be : data-uri and default network
// we should not use file-scheme as it's a bit complicated to assume file usage (lack of permissions)
addSchemeHandler(DataUriSchemeHandler.create());
addSchemeHandler(NetworkSchemeHandler.create());
}
void executorService(@NonNull ExecutorService executorService) { void executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService; this.executorService = executorService;
} }
@ -66,12 +78,6 @@ class AsyncDrawableLoaderBuilder {
isBuilt = true; isBuilt = true;
// we must have schemeHandlers registered (we will provide
// default media decoder if it's absent)
if (schemeHandlers.size() == 0) {
return new AsyncDrawableLoaderNoOp();
}
// @since 4.0.0-SNAPSHOT // @since 4.0.0-SNAPSHOT
if (defaultMediaDecoder == null) { if (defaultMediaDecoder == null) {
defaultMediaDecoder = DefaultImageMediaDecoder.create(); defaultMediaDecoder = DefaultImageMediaDecoder.create();
@ -83,5 +89,4 @@ class AsyncDrawableLoaderBuilder {
return new AsyncDrawableLoaderImpl(this); return new AsyncDrawableLoaderImpl(this);
} }
} }

View File

@ -14,6 +14,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonSpansFactory;
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"})
public class ImagesPlugin extends AbstractMarkwonPlugin { public class ImagesPlugin extends AbstractMarkwonPlugin {
/** /**
@ -91,6 +92,12 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return this; return this;
} }
/**
* @see DefaultImageMediaDecoder
* @see ru.noties.markwon.image.svg.SvgMediaDecoder
* @see ru.noties.markwon.image.gif.GifMediaDecoder
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { public ImagesPlugin addMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
checkBuilderState(); checkBuilderState();
@ -98,13 +105,23 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return this; return this;
} }
/**
* Please note that if not specified a {@link DefaultImageMediaDecoder} will be used. So
* if you need to disable default-image-media-decoder specify here own no-op implementation.
*
* @see DefaultImageMediaDecoder
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { public ImagesPlugin defaultMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
checkBuilderState(); checkBuilderState();
builder.defaultMediaDecoder(mediaDecoder); builder.defaultMediaDecoder(mediaDecoder);
return this; return this;
} }
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin removeSchemeHandler(@NonNull String scheme) { public ImagesPlugin removeSchemeHandler(@NonNull String scheme) {
checkBuilderState(); checkBuilderState();
@ -112,6 +129,9 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return this; return this;
} }
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin removeMediaDecoder(@NonNull String contentType) { public ImagesPlugin removeMediaDecoder(@NonNull String contentType) {
checkBuilderState(); checkBuilderState();
@ -119,6 +139,9 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return this; return this;
} }
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin placeholderProvider(@NonNull PlaceholderProvider placeholderProvider) { public ImagesPlugin placeholderProvider(@NonNull PlaceholderProvider placeholderProvider) {
checkBuilderState(); checkBuilderState();
@ -126,6 +149,10 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return this; return this;
} }
/**
* @see ErrorHandler
* @since 4.0.0-SNAPSHOT
*/
@NonNull @NonNull
public ImagesPlugin errorHandler(@NonNull ErrorHandler errorHandler) { public ImagesPlugin errorHandler(@NonNull ErrorHandler errorHandler) {
checkBuilderState(); checkBuilderState();

View File

@ -0,0 +1,194 @@
package ru.noties.markwon.image;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import ru.noties.markwon.image.data.DataUriSchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class AsyncDrawableLoaderBuilderTest {
private AsyncDrawableLoaderBuilder builder;
@Before
public void before() {
builder = new AsyncDrawableLoaderBuilder();
}
@Test
public void default_scheme_handlers() {
// builder adds default data-uri and network scheme-handlers
final String[] registered = {
DataUriSchemeHandler.SCHEME,
NetworkSchemeHandler.SCHEME_HTTP,
NetworkSchemeHandler.SCHEME_HTTPS
};
for (String scheme : registered) {
assertNotNull(scheme, builder.schemeHandlers.get(scheme));
}
}
@Test
public void built_flag() {
// isBuilt flag must be set after `build` method call
assertFalse(builder.isBuilt);
builder.build();
assertTrue(builder.isBuilt);
}
@Test
public void defaults_initialized() {
// default-media-decoder and executor-service must be initialized
assertNull(builder.defaultMediaDecoder);
assertNull(builder.executorService);
builder.build();
assertNotNull(builder.defaultMediaDecoder);
assertNotNull(builder.executorService);
}
@Test
public void executor() {
// supplied executor-service must be used
assertNull(builder.executorService);
final ExecutorService service = mock(ExecutorService.class);
builder.executorService(service);
builder.build();
assertEquals(service, builder.executorService);
}
@Test
public void add_scheme_handler() {
final String scheme = "mock";
assertNull(builder.schemeHandlers.get(scheme));
final SchemeHandler schemeHandler = mock(SchemeHandler.class);
when(schemeHandler.supportedSchemes()).thenReturn(Collections.singleton(scheme));
builder.addSchemeHandler(schemeHandler);
builder.build();
assertEquals(schemeHandler, builder.schemeHandlers.get(scheme));
}
@Test
public void add_scheme_handler_multiple_types() {
// all supported types are registered
final String[] schemes = {
"mock-1",
"mock-2"
};
final SchemeHandler schemeHandler = mock(SchemeHandler.class);
when(schemeHandler.supportedSchemes()).thenReturn(Arrays.asList(schemes));
builder.addSchemeHandler(schemeHandler);
for (String scheme : schemes) {
assertEquals(scheme, schemeHandler, builder.schemeHandlers.get(scheme));
}
}
@Test
public void add_media_decoder() {
final String media = "mocked/type";
assertNull(builder.mediaDecoders.get(media));
final MediaDecoder mediaDecoder = mock(MediaDecoder.class);
when(mediaDecoder.supportedTypes()).thenReturn(Collections.singleton(media));
builder.addMediaDecoder(mediaDecoder);
builder.build();
assertEquals(mediaDecoder, builder.mediaDecoders.get(media));
}
@Test
public void add_media_decoder_multiple_types() {
final String[] types = {
"mock/type1",
"mock/type2"
};
final MediaDecoder mediaDecoder = mock(MediaDecoder.class);
when(mediaDecoder.supportedTypes()).thenReturn(Arrays.asList(types));
builder.addMediaDecoder(mediaDecoder);
for (String type : types) {
assertEquals(type, mediaDecoder, builder.mediaDecoders.get(type));
}
}
@Test
public void default_media_decoder() {
assertNull(builder.defaultMediaDecoder);
final MediaDecoder mediaDecoder = mock(MediaDecoder.class);
builder.defaultMediaDecoder(mediaDecoder);
builder.build();
assertEquals(mediaDecoder, builder.defaultMediaDecoder);
}
@Test
public void remove_scheme_handler() {
final String scheme = "mock";
final SchemeHandler schemeHandler = mock(SchemeHandler.class);
when(schemeHandler.supportedSchemes()).thenReturn(Collections.singleton(scheme));
assertNull(builder.schemeHandlers.get(scheme));
builder.addSchemeHandler(schemeHandler);
assertNotNull(builder.schemeHandlers.get(scheme));
builder.removeSchemeHandler(scheme);
assertNull(builder.schemeHandlers.get(scheme));
}
@Test
public void remove_media_decoder() {
final String media = "mock/type";
final MediaDecoder mediaDecoder = mock(MediaDecoder.class);
when(mediaDecoder.supportedTypes()).thenReturn(Collections.singleton(media));
assertNull(builder.mediaDecoders.get(media));
builder.addMediaDecoder(mediaDecoder);
assertNotNull(builder.mediaDecoders.get(media));
builder.removeMediaDecoder(media);
assertNull(builder.mediaDecoders.get(media));
}
}

View File

@ -0,0 +1,177 @@
package ru.noties.markwon.image;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.widget.TextView;
import org.commonmark.node.Image;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.concurrent.ExecutorService;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.image.data.DataUriSchemeHandler;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
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)
public class ImagesPluginTest {
private ImagesPlugin plugin;
@Before
public void before() {
plugin = ImagesPlugin.create();
}
@Test
public void build_state() {
// it's not possible to mutate images-plugin after `configureConfiguration` call
// validate that it doesn't throw here
plugin.addSchemeHandler(DataUriSchemeHandler.create());
// mark the state
plugin.configureConfiguration(mock(MarkwonConfiguration.Builder.class));
final class Throws {
private void assertThrows(@NonNull Runnable action) {
//noinspection CatchMayIgnoreException
try {
action.run();
fail();
} catch (Throwable t) {
assertTrue(t.getMessage(), t.getMessage().contains("ImagesPlugin has already been configured"));
}
}
}
final Throws check = new Throws();
// executor-service
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.executorService(mock(ExecutorService.class));
}
});
// add-scheme-handler
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.addSchemeHandler(mock(SchemeHandler.class));
}
});
// add-media-decoder
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.addMediaDecoder(mock(MediaDecoder.class));
}
});
// default-media-decoder
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.defaultMediaDecoder(mock(MediaDecoder.class));
}
});
// remove-scheme-handler
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.removeSchemeHandler("mock");
}
});
// remove-media-decoder
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.removeMediaDecoder("mock/type");
}
});
// placeholder-provider
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.placeholderProvider(mock(ImagesPlugin.PlaceholderProvider.class));
}
});
// error-handler
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.errorHandler(mock(ImagesPlugin.ErrorHandler.class));
}
});
// final check if for actual `configureConfiguration` call (must be called only once)
check.assertThrows(new Runnable() {
@Override
public void run() {
plugin.configureConfiguration(mock(MarkwonConfiguration.Builder.class));
}
});
}
@Test
public void image_span_factory_registered() {
final MarkwonSpansFactory.Builder builder = mock(MarkwonSpansFactory.Builder.class);
plugin.configureSpansFactory(builder);
final ArgumentCaptor<SpanFactory> captor = ArgumentCaptor.forClass(SpanFactory.class);
verify(builder, times(1))
.setFactory(eq(Image.class), captor.capture());
assertNotNull(captor.getValue());
}
@Test
public void before_set_text() {
// verify that AsyncDrawableScheduler is called
final TextView textView = mock(TextView.class);
plugin.beforeSetText(textView, mock(Spanned.class));
verify(textView, times(1))
.getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode));
}
@Test
public void after_set_text() {
// verify that AsyncDrawableScheduler is called
final TextView textView = mock(TextView.class);
when(textView.getText()).thenReturn("some text");
plugin.afterSetText(textView);
verify(textView, times(1))
.getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode));
}
}

View File

@ -0,0 +1,5 @@
# Linkify
Use this module (or take a hint from it) if you would need _linkify_ capabilities. Do not
use `TextView.setAutolinkMask` (or specify `autolink` in XML) because it will remove all
existing links and keep only the ones it creates.

View File

@ -18,8 +18,7 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
@IntDef(flag = true, value = { @IntDef(flag = true, value = {
Linkify.EMAIL_ADDRESSES, Linkify.EMAIL_ADDRESSES,
Linkify.PHONE_NUMBERS, Linkify.PHONE_NUMBERS,
Linkify.WEB_URLS, Linkify.WEB_URLS
Linkify.ALL
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@interface LinkifyMask { @interface LinkifyMask {