Add tests for search functionality in SFragment (#617)
* Add tests for search functionality in SFragment * Parameterize url matching tests * Clean up / compartmentalize search tests * Make SFragmentTest filesystem location match package name
This commit is contained in:
parent
5cfe6f8fa5
commit
0aeab2a983
2 changed files with 327 additions and 11 deletions
|
@ -266,7 +266,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
// https://pleroma.foo.bar/users/43456787654678
|
// https://pleroma.foo.bar/users/43456787654678
|
||||||
// https://pleroma.foo.bar/notice/43456787654678
|
// https://pleroma.foo.bar/notice/43456787654678
|
||||||
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||||
private static boolean looksLikeMastodonUrl(String urlString) {
|
static boolean looksLikeMastodonUrl(String urlString) {
|
||||||
URI uri;
|
URI uri;
|
||||||
try {
|
try {
|
||||||
uri = new URI(urlString);
|
uri = new URI(urlString);
|
||||||
|
@ -281,26 +281,27 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = uri.getPath();
|
String path = uri.getPath();
|
||||||
return path.matches("^/@[^/]*$") ||
|
return path.matches("^/@[^/]+$") ||
|
||||||
path.matches("^/users/[^/]+$") ||
|
path.matches("^/users/[^/]+$") ||
|
||||||
path.matches("^/(@|notice)[^/]*/\\d+$") ||
|
path.matches("^/@[^/]+/\\d+$") ||
|
||||||
|
path.matches("^/notice/\\d+$") ||
|
||||||
path.matches("^/objects/[-a-f0-9]+$");
|
path.matches("^/objects/[-a-f0-9]+$");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBeginSearch(@NonNull String url) {
|
void onBeginSearch(@NonNull String url) {
|
||||||
searchUrl = url;
|
searchUrl = url;
|
||||||
showQuerySheet();
|
showQuerySheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getCancelSearchRequested(@NonNull String url) {
|
boolean getCancelSearchRequested(@NonNull String url) {
|
||||||
return !url.equals(searchUrl);
|
return !url.equals(searchUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSearching() {
|
boolean isSearching() {
|
||||||
return searchUrl != null;
|
return searchUrl != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onEndSearch(@NonNull String url) {
|
void onEndSearch(@NonNull String url) {
|
||||||
if (url.equals(searchUrl)) {
|
if (url.equals(searchUrl)) {
|
||||||
// Don't clear query if there's no match,
|
// Don't clear query if there's no match,
|
||||||
// since we might just now be getting the response for a canceled search
|
// since we might just now be getting the response for a canceled search
|
||||||
|
@ -309,16 +310,20 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelActiveSearch()
|
void cancelActiveSearch()
|
||||||
{
|
{
|
||||||
if (isSearching()) {
|
if (isSearching()) {
|
||||||
onEndSearch(searchUrl);
|
onEndSearch(searchUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void openLink(@NonNull String url) {
|
||||||
|
LinkHelper.openLink(url, getContext());
|
||||||
|
}
|
||||||
|
|
||||||
public void onViewURL(String url) {
|
public void onViewURL(String url) {
|
||||||
if (!looksLikeMastodonUrl(url)) {
|
if (!looksLikeMastodonUrl(url)) {
|
||||||
LinkHelper.openLink(url, getContext());
|
openLink(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,14 +351,14 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LinkHelper.openLink(url, getContext());
|
openLink(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<SearchResults> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<SearchResults> call, @NonNull Throwable t) {
|
||||||
if (!getCancelSearchRequested(url)) {
|
if (!getCancelSearchRequested(url)) {
|
||||||
onEndSearch(url);
|
onEndSearch(url);
|
||||||
LinkHelper.openLink(url, getContext());
|
openLink(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
/* Copyright 2018 Levi Bard
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.fragment
|
||||||
|
|
||||||
|
import android.text.SpannedString
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.SearchResults
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.*
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SFragmentTest {
|
||||||
|
private lateinit var fragment : FakeSFragment
|
||||||
|
private lateinit var apiMock: MastodonApi
|
||||||
|
private val accountQuery = "http://mastodon.foo.bar/@User"
|
||||||
|
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
||||||
|
private val nonMastodonQuery = "http://medium.com/@correspondent/345678"
|
||||||
|
private val emptyCallback = FakeSearchResults()
|
||||||
|
|
||||||
|
private val account = Account (
|
||||||
|
"1",
|
||||||
|
"admin",
|
||||||
|
"admin",
|
||||||
|
"Ad Min",
|
||||||
|
SpannedString(""),
|
||||||
|
"http://mastodon.foo.bar",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
private val accountCallback = FakeSearchResults(account)
|
||||||
|
|
||||||
|
private val status = Status(
|
||||||
|
"1",
|
||||||
|
statusQuery,
|
||||||
|
account,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
SpannedString("omgwat"),
|
||||||
|
Date(),
|
||||||
|
Collections.emptyList(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
Status.Visibility.PUBLIC,
|
||||||
|
arrayOf(),
|
||||||
|
arrayOf(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
private val statusCallback = FakeSearchResults(status)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
fragment = FakeSFragment()
|
||||||
|
|
||||||
|
apiMock = Mockito.mock(MastodonApi::class.java)
|
||||||
|
`when`(apiMock.search(eq(accountQuery), ArgumentMatchers.anyBoolean())).thenReturn(accountCallback)
|
||||||
|
`when`(apiMock.search(eq(statusQuery), ArgumentMatchers.anyBoolean())).thenReturn(statusCallback)
|
||||||
|
`when`(apiMock.search(eq(nonMastodonQuery), ArgumentMatchers.anyBoolean())).thenReturn(emptyCallback)
|
||||||
|
fragment.mastodonApi = apiMock
|
||||||
|
}
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class UrlMatchingTests(val url: String, val expectedResult: Boolean) {
|
||||||
|
companion object {
|
||||||
|
@Parameterized.Parameters(name = "match_{0}")
|
||||||
|
@JvmStatic
|
||||||
|
fun data() : Iterable<Any> {
|
||||||
|
return listOf(
|
||||||
|
arrayOf("https://mastodon.foo.bar/@User", true),
|
||||||
|
arrayOf("http://mastodon.foo.bar/@abc123", true),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@user/3", true),
|
||||||
|
arrayOf("https://pleroma.foo.bar/users/meh3223", true),
|
||||||
|
arrayOf("https://pleroma.foo.bar/users/2345", true),
|
||||||
|
arrayOf("https://pleroma.foo.bar/notice/9", true),
|
||||||
|
arrayOf("https://pleroma.foo.bar/notice/9345678", true),
|
||||||
|
arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
|
||||||
|
arrayOf("https://google.com/", false),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@User#foo", false),
|
||||||
|
arrayOf("http://mastodon.foo.bar/@", false),
|
||||||
|
arrayOf("http://mastodon.foo.bar/@/345678", false),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
|
||||||
|
arrayOf("https://mastodon.foo.bar/@user/3abce", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/users/", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/user/2345", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/notice/wat", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/notices/123456", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
|
||||||
|
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
Assert.assertEquals(expectedResult, SFragment.looksLikeMastodonUrl(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
|
||||||
|
fragment.onBeginSearch("https://mastodon.foo.bar/@User")
|
||||||
|
Assert.assertTrue(fragment.isSearching)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() {
|
||||||
|
val validUrl = "https://mastodon.foo.bar/@User"
|
||||||
|
fragment.onBeginSearch(validUrl)
|
||||||
|
fragment.onEndSearch(validUrl)
|
||||||
|
Assert.assertFalse(fragment.isSearching)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() {
|
||||||
|
val validUrl = "https://mastodon.foo.bar/@User"
|
||||||
|
val invalidUrl = ""
|
||||||
|
|
||||||
|
fragment.onBeginSearch(validUrl)
|
||||||
|
fragment.onEndSearch(invalidUrl)
|
||||||
|
Assert.assertTrue(fragment.isSearching)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelActiveSearch() {
|
||||||
|
val url = "https://mastodon.foo.bar/@User"
|
||||||
|
|
||||||
|
fragment.onBeginSearch(url)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
Assert.assertFalse(fragment.isSearching)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getCancelSearchRequested_detectsURL() {
|
||||||
|
val firstUrl = "https://mastodon.foo.bar/@User"
|
||||||
|
val secondUrl = "https://mastodon.foo.bar/@meh"
|
||||||
|
|
||||||
|
fragment.onBeginSearch(firstUrl)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
|
||||||
|
fragment.onBeginSearch(secondUrl)
|
||||||
|
Assert.assertTrue(fragment.getCancelSearchRequested(firstUrl))
|
||||||
|
Assert.assertFalse(fragment.getCancelSearchRequested(secondUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
||||||
|
fragment.onViewURL(accountQuery)
|
||||||
|
accountCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(account.id, fragment.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||||
|
fragment.onViewURL(statusQuery)
|
||||||
|
statusCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(status, fragment.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
||||||
|
fragment.onViewURL(nonMastodonQuery)
|
||||||
|
emptyCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(nonMastodonQuery, fragment.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_withCancellation_doesNotLoadUrl_forAccount() {
|
||||||
|
fragment.onViewURL(accountQuery)
|
||||||
|
Assert.assertTrue(fragment.isSearching)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
Assert.assertFalse(fragment.isSearching)
|
||||||
|
accountCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(null, fragment.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
||||||
|
fragment.onViewURL(accountQuery)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
accountCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(null, fragment.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
||||||
|
fragment.onViewURL(nonMastodonQuery)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
emptyCallback.invokeCallback()
|
||||||
|
Assert.assertEquals(null, fragment.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_withPreviousCancellation_completes() {
|
||||||
|
// begin/cancel account search
|
||||||
|
fragment.onViewURL(accountQuery)
|
||||||
|
fragment.cancelActiveSearch()
|
||||||
|
|
||||||
|
// begin status search
|
||||||
|
fragment.onViewURL(statusQuery)
|
||||||
|
|
||||||
|
// return response from account search
|
||||||
|
accountCallback.invokeCallback()
|
||||||
|
|
||||||
|
// ensure that status search is still ongoing
|
||||||
|
Assert.assertTrue(fragment.isSearching)
|
||||||
|
statusCallback.invokeCallback()
|
||||||
|
|
||||||
|
// ensure that the result of the status search was recorded
|
||||||
|
// and the account search wasn't
|
||||||
|
Assert.assertEquals(status, fragment.status)
|
||||||
|
Assert.assertEquals(null, fragment.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeSearchResults : Call<SearchResults>
|
||||||
|
{
|
||||||
|
private var searchResults: SearchResults
|
||||||
|
private var callback: Callback<SearchResults>? = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
searchResults = SearchResults(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(status: Status) {
|
||||||
|
searchResults = SearchResults(Collections.emptyList(), listOf(status), Collections.emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(account: Account) {
|
||||||
|
searchResults = SearchResults(listOf(account), Collections.emptyList(), Collections.emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invokeCallback() {
|
||||||
|
callback?.onResponse(this, Response.success(searchResults))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enqueue(callback: Callback<SearchResults>?) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExecuted(): Boolean { throw NotImplementedError() }
|
||||||
|
override fun clone(): Call<SearchResults> { throw NotImplementedError() }
|
||||||
|
override fun isCanceled(): Boolean { throw NotImplementedError() }
|
||||||
|
override fun cancel() { throw NotImplementedError() }
|
||||||
|
override fun execute(): Response<SearchResults> { throw NotImplementedError() }
|
||||||
|
override fun request(): Request { throw NotImplementedError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeSFragment : SFragment() {
|
||||||
|
var status: Status? = null
|
||||||
|
var accountId: String? = null
|
||||||
|
var url: String? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
callList = mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openLink(url: String) {
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewAccount(id: String?) {
|
||||||
|
accountId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewThread(status: Status?) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeItem(position: Int) { throw NotImplementedError() }
|
||||||
|
override fun removeAllByAccountId(accountId: String?) { throw NotImplementedError() }
|
||||||
|
override fun timelineCases(): TimelineCases { throw NotImplementedError() }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue