Sample app, all samples test

This commit is contained in:
Dimitry Ivanov 2020-07-11 19:52:12 +03:00
parent c96ea690f6
commit df23339dba
9 changed files with 198 additions and 14 deletions

View File

@ -1,5 +1,33 @@
# Markwon sample app # Markwon sample app
## Distribution
Sample app is distributed via special parent-less branch [sample-store](https://github.com/noties/Markwon/tree/sample-store).
Inside the app, under version badges, tap `CHECK FOR UPDATES` to check for updates. Sample app
is not attached to main libraries versions and can be _released_ independently.
Application is signed with `keystore.jks`, which fingerprints are:
* __SHA1__: `BA:70:A5:D2:40:65:F1:FA:88:90:59:BA:FC:B7:31:81:E6:37:D9:41`
* __SHA256__: `82:C9:61:C5:DF:35:B1:CB:29:D5:48:83:FB:EB:9F:3E:7D:52:67:63:4F:D2:CE:0A:2D:70:17:85:FF:48:67:51`
[Download latest APK](https://github.com/noties/Markwon/raw/sample-store/markwon-debug.apk)
## Deeplink
Sample app handles special `markwon` scheme:
* `markwon://sample/{ID}` to open specific sample given the `{ID}`
* `markwon://search?q={TEXT TO SEARCH}&a={ARTIFACT}&t={TAG}`
Please note that search deeplink can have one of type: artifact or tag (if both are specified artifact will be used).
To test locally:
```
adb shell am start -a android.intent.action.ACTION_VIEW -d markwon://sample/ID
```
Please note that you might need to _url encode_ the `-d` argument
## Building ## Building
When adding/removing samples _most likely_ a clean build would be required. When adding/removing samples _most likely_ a clean build would be required.
@ -7,6 +35,22 @@ First, for annotation processor to create `samples.json`. And secondly,
in order for Android Gradle plugin to bundle resources references via in order for Android Gradle plugin to bundle resources references via
symbolic links (the `sample.json` itself and `io.noties.markwon.app.samples.*` directory) symbolic links (the `sample.json` itself and `io.noties.markwon.app.samples.*` directory)
```gradle ```
./gradlew :app-s:clean :app-s:asDe ./gradlew :app-s:clean :app-s:asDe
``` ```
## Tests
This app uses [Robolectric](https://robolectric.org)(v3.8) for tests which is incompatible
with JDK > 1.8. In order to run tests from command line with IDEA-bundled JDK - a special argument is
required:
```
./gradlew :app-s:testDe -Dorg.gradle.java.home="{INSERT BUNDLED JDK PATH HERE}"
```
To obtain bundled JDK:
* open `Project Structure...`
* open `SDK Location`
* copy contents of the field under `JDK Location`

View File

@ -139,4 +139,10 @@ dependencies {
implementation it['android-svg'] implementation it['android-svg']
implementation it['android-gif-impl'] implementation it['android-gif-impl']
} }
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
}
} }

View File

@ -14,7 +14,8 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@Suppress("unused") @Suppress("unused")
class App : Application() { // `open` is required for tests (to create a spy mockito instance)
open class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View File

@ -26,7 +26,9 @@ public class SimpleExtensionSample extends MarkwonTextViewSample {
"# SimpleExt\n" + "# SimpleExt\n" +
"\n" + "\n" +
"+let's start with `+`, ??then we can use this, and finally @@this$$??+"; "+let's start with `+`, ??then we can use this, and finally @@this$$??+";
;
// NB! we cannot have multiple delimiter processor with the same character
// (even if lengths are different)
final Markwon markwon = Markwon.builder(context) final Markwon markwon = Markwon.builder(context)
.usePlugin(SimpleExtPlugin.create(plugin -> { .usePlugin(SimpleExtPlugin.create(plugin -> {
@ -36,7 +38,7 @@ public class SimpleExtensionSample extends MarkwonTextViewSample {
.addExtension( .addExtension(
2, 2,
'@', '@',
'?', '$',
(configuration, props) -> new ForegroundColorSpan(Color.RED) (configuration, props) -> new ForegroundColorSpan(Color.RED)
); );
})) }))

View File

@ -162,7 +162,7 @@ public class HtmlDetailsSample extends MarkwonSample {
private TextView appendTextView() { private TextView appendTextView() {
final View view = LayoutInflater.from(context) final View view = LayoutInflater.from(context)
.inflate(R.layout.view_html_details_text_view, content, false); .inflate(R.layout.view_html_details_text_view, content, false);
final TextView textView = view.findViewById(R.id.text); final TextView textView = view.findViewById(R.id.text_view);
content.addView(view); content.addView(view);
return textView; return textView;
} }

View File

@ -19,19 +19,24 @@ public abstract class SampleUtils {
@NonNull @NonNull
public static List<Sample> readSamples(@NonNull Context context) { public static List<Sample> readSamples(@NonNull Context context) {
final Gson gson = new Gson();
try (InputStream inputStream = context.getAssets().open("samples.json")) { try (InputStream inputStream = context.getAssets().open("samples.json")) {
return gson.fromJson( return readSamples(inputStream);
new InputStreamReader(inputStream),
new TypeToken<List<Sample>>() {
}.getType()
);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
// NB! stream is not closed by this method
@NonNull
public static List<Sample> readSamples(@NonNull InputStream inputStream) {
final Gson gson = new Gson();
return gson.fromJson(
new InputStreamReader(inputStream),
new TypeToken<List<Sample>>() {
}.getType()
);
}
private SampleUtils() { private SampleUtils() {
} }
} }

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text" android:id="@+id/text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dip" android:padding="8dip"

View File

@ -0,0 +1,125 @@
package io.noties.markwon.app
import android.content.Context
import android.os.Build
import android.text.SpannableString
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.sample.ui.MarkwonSample
import io.noties.markwon.app.utils.SampleUtils
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.RETURNS_MOCKS
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(org.robolectric.ParameterizedRobolectricTestRunner::class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = [Build.VERSION_CODES.O])
class AllSamples(private val sample: Sample) {
@Test
fun sample() {
val markwonSample: MarkwonSample = Class.forName(sample.javaClassName).newInstance() as MarkwonSample
val inflater = mock(LayoutInflater::class.java).apply {
// mock must be initialized (_finished_) before we
// can start `thenReturn` or creating another mock
val view = view
val inflater = this
// html-details require this, it is creating views manually
val context = spy(RuntimeEnvironment.application).apply {
`when`(getSystemService(eq(Context.LAYOUT_INFLATER_SERVICE)))
.thenReturn(inflater)
}
`when`(view.context).thenReturn(context)
`when`(this.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
.thenReturn(view)
}
val view = markwonSample.createView(
inflater,
mock(ViewGroup::class.java))
markwonSample.onViewCreated(view)
}
private val view: View
get() {
val view: View = mock(View::class.java)
view.apply {
// textView
val textView = textView
`when`(findViewById<TextView>(eq(R.id.text_view)))
.thenReturn(textView)
`when`(findViewById<EditText>(eq(R.id.edit_text)))
.thenReturn(mock(EditText::class.java))
// scrollView
`when`(findViewById<ScrollView>(eq(R.id.scroll_view)))
.thenReturn(mock(ScrollView::class.java))
// recyclerView
`when`(findViewById<RecyclerView>(eq(R.id.recycler_view)))
.thenReturn(mock(RecyclerView::class.java))
// html-details ViewGroup
`when`(findViewById<ViewGroup>(R.id.content))
.thenReturn(mock(ViewGroup::class.java))
// special editor views
arrayOf(
R.id.bold,
R.id.italic,
R.id.strike,
R.id.quote,
R.id.code)
.forEach {
val button = mock(Button::class.java).apply {
`when`(text).thenReturn("")
}
`when`(findViewById<Button>(eq(it)))
.thenReturn(button)
}
}
return view
}
private val textView: TextView
get() {
val textView: TextView = mock(TextView::class.java, RETURNS_MOCKS)
textView.apply {
`when`(text)
.thenReturn(SpannableString(""))
`when`(getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode)))
.thenReturn(0)
}
return textView
}
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{index}: {0}")
public fun samples(): Collection<Any> {
return AllSamples::class.java.classLoader!!.getResourceAsStream("samples.json").use { inputStream ->
SampleUtils
.readSamples(inputStream)
.map { arrayOf<Any>(it) }
}
}
}
}

View File

@ -0,0 +1 @@
../../../samples.json