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/notice/43456787654678
|
||||
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||
private static boolean looksLikeMastodonUrl(String urlString) {
|
||||
static boolean looksLikeMastodonUrl(String urlString) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
|
@ -281,26 +281,27 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
|||
}
|
||||
|
||||
String path = uri.getPath();
|
||||
return path.matches("^/@[^/]*$") ||
|
||||
return path.matches("^/@[^/]+$") ||
|
||||
path.matches("^/users/[^/]+$") ||
|
||||
path.matches("^/(@|notice)[^/]*/\\d+$") ||
|
||||
path.matches("^/@[^/]+/\\d+$") ||
|
||||
path.matches("^/notice/\\d+$") ||
|
||||
path.matches("^/objects/[-a-f0-9]+$");
|
||||
}
|
||||
|
||||
private void onBeginSearch(@NonNull String url) {
|
||||
void onBeginSearch(@NonNull String url) {
|
||||
searchUrl = url;
|
||||
showQuerySheet();
|
||||
}
|
||||
|
||||
private boolean getCancelSearchRequested(@NonNull String url) {
|
||||
boolean getCancelSearchRequested(@NonNull String url) {
|
||||
return !url.equals(searchUrl);
|
||||
}
|
||||
|
||||
private boolean isSearching() {
|
||||
boolean isSearching() {
|
||||
return searchUrl != null;
|
||||
}
|
||||
|
||||
private void onEndSearch(@NonNull String url) {
|
||||
void onEndSearch(@NonNull String url) {
|
||||
if (url.equals(searchUrl)) {
|
||||
// Don't clear query if there's no match,
|
||||
// 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()) {
|
||||
onEndSearch(searchUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void openLink(@NonNull String url) {
|
||||
LinkHelper.openLink(url, getContext());
|
||||
}
|
||||
|
||||
public void onViewURL(String url) {
|
||||
if (!looksLikeMastodonUrl(url)) {
|
||||
LinkHelper.openLink(url, getContext());
|
||||
openLink(url);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -346,14 +351,14 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
|||
return;
|
||||
}
|
||||
}
|
||||
LinkHelper.openLink(url, getContext());
|
||||
openLink(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<SearchResults> call, @NonNull Throwable t) {
|
||||
if (!getCancelSearchRequested(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