Merge tag 'v25.2' into develop
# Conflicts: # README.md # app/build.gradle # app/lint-baseline.xml # app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt # app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt # app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt # app/src/main/res/layout/activity_about.xml # app/src/main/res/layout/item_emoji_pref.xml # app/src/main/res/values-ar/strings.xml # app/src/main/res/values-bg/strings.xml # app/src/main/res/values-cy/strings.xml # app/src/main/res/values-de/strings.xml # app/src/main/res/values-fa/strings.xml # app/src/main/res/values-gd/strings.xml # app/src/main/res/values-gl/strings.xml # app/src/main/res/values-hu/strings.xml # app/src/main/res/values-is/strings.xml # app/src/main/res/values-it/strings.xml # app/src/main/res/values-ja/strings.xml # app/src/main/res/values-nl/strings.xml # app/src/main/res/values-oc/strings.xml # app/src/main/res/values-pt-rBR/strings.xml # app/src/main/res/values-pt-rPT/strings.xml # app/src/main/res/values-ru/strings.xml # app/src/main/res/values-si/strings.xml # app/src/main/res/values-sv/strings.xml # app/src/main/res/values-tr/strings.xml # app/src/main/res/values-uk/strings.xml # app/src/main/res/values-vi/strings.xml # app/src/main/res/values-zh-rCN/strings.xml # app/src/main/res/values/strings.xml # fastlane/metadata/android/ru/full_description.txt # fastlane/metadata/android/zh-Hans/full_description.txt
This commit is contained in:
parent
84670dbc0b
commit
875013e47f
630 changed files with 22153 additions and 18732 deletions
|
|
@ -16,14 +16,19 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
@ -34,9 +39,8 @@ import org.mockito.ArgumentMatchers.anyBoolean
|
|||
import org.mockito.Mockito.eq
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class BottomSheetActivityTest {
|
||||
|
||||
@get:Rule
|
||||
|
|
@ -48,8 +52,7 @@ class BottomSheetActivityTest {
|
|||
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
||||
private val nonexistentStatusQuery = "http://mastodon.foo.bar/@User/345678000"
|
||||
private val nonMastodonQuery = "http://medium.com/@correspondent/345678"
|
||||
private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList()))
|
||||
private val testScheduler = TestScheduler()
|
||||
private val emptyResult = NetworkResult.success(SearchResult(emptyList(), emptyList(), emptyList()))
|
||||
|
||||
private val account = TimelineAccount(
|
||||
id = "1",
|
||||
|
|
@ -60,7 +63,7 @@ class BottomSheetActivityTest {
|
|||
url = "http://mastodon.foo.bar/@User",
|
||||
avatar = ""
|
||||
)
|
||||
private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList()))
|
||||
private val accountResult = NetworkResult.success(SearchResult(listOf(account), emptyList(), emptyList()))
|
||||
|
||||
private val status = Status(
|
||||
id = "1",
|
||||
|
|
@ -91,20 +94,17 @@ class BottomSheetActivityTest {
|
|||
poll = null,
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
)
|
||||
private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList()))
|
||||
private val statusResult = NetworkResult.success(SearchResult(emptyList(), listOf(status), emptyList()))
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler }
|
||||
|
||||
apiMock = mock {
|
||||
on { searchObservable(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
|
||||
on { searchObservable(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusSingle
|
||||
on { searchObservable(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
|
||||
on { searchObservable(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyCallback
|
||||
onBlocking { search(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResult
|
||||
onBlocking { search(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusResult
|
||||
onBlocking { search(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResult
|
||||
onBlocking { search(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyResult
|
||||
}
|
||||
|
||||
activity = FakeBottomSheetActivity(apiMock)
|
||||
|
|
@ -157,86 +157,134 @@ class BottomSheetActivityTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
||||
activity.viewUrl(accountQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
assertEquals(account.id, activity.accountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||
activity.viewUrl(statusQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
assertEquals(status.id, activity.statusId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withNoResults_appliesRequestedFallbackBehavior() {
|
||||
for (fallbackBehavior in listOf(PostLookupFallbackBehavior.OPEN_IN_BROWSER, PostLookupFallbackBehavior.DISPLAY_ERROR)) {
|
||||
activity.viewUrl(nonMastodonQuery, fallbackBehavior)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
assertEquals(fallbackBehavior, activity.fallbackBehavior)
|
||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(accountQuery)
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
assertEquals(account.id, activity.accountId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_doesNotRespectUnrelatedResult() {
|
||||
activity.viewUrl(nonexistentStatusQuery)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
assertEquals(nonexistentStatusQuery, activity.link)
|
||||
assertEquals(null, activity.accountId)
|
||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(statusQuery)
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
assertEquals(status.id, activity.statusId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forAccount() {
|
||||
activity.viewUrl(accountQuery)
|
||||
assertTrue(activity.isSearching())
|
||||
activity.cancelActiveSearch()
|
||||
assertFalse(activity.isSearching())
|
||||
assertEquals(null, activity.accountId)
|
||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.accountId)
|
||||
fun search_withNoResults_appliesRequestedFallbackBehavior() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
for (fallbackBehavior in listOf(
|
||||
PostLookupFallbackBehavior.OPEN_IN_BROWSER,
|
||||
PostLookupFallbackBehavior.DISPLAY_ERROR
|
||||
)) {
|
||||
activity.viewUrl(nonMastodonQuery, fallbackBehavior)
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
assertEquals(nonMastodonQuery, activity.link)
|
||||
assertEquals(fallbackBehavior, activity.fallbackBehavior)
|
||||
}
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.searchUrl)
|
||||
fun search_doesNotRespectUnrelatedResult() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(nonexistentStatusQuery)
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
assertEquals(nonexistentStatusQuery, activity.link)
|
||||
assertEquals(null, activity.accountId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_withPreviousCancellation_completes() {
|
||||
// begin/cancel account search
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
fun search_withCancellation_doesNotLoadUrl_forAccount() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(accountQuery)
|
||||
assertTrue(activity.isSearching())
|
||||
activity.cancelActiveSearch()
|
||||
assertFalse(activity.isSearching())
|
||||
assertEquals(null, activity.accountId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
// begin status search
|
||||
activity.viewUrl(statusQuery)
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forStatus() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.accountId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that search is still ongoing
|
||||
assertTrue(activity.isSearching())
|
||||
@Test
|
||||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
activity.cancelActiveSearch()
|
||||
assertEquals(null, activity.searchUrl)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
// return searchResults
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
@Test
|
||||
fun search_withPreviousCancellation_completes() = runTest {
|
||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||
try {
|
||||
// begin/cancel account search
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
|
||||
// ensure that the result of the status search was recorded
|
||||
// and the account search wasn't
|
||||
assertEquals(status.id, activity.statusId)
|
||||
assertEquals(null, activity.accountId)
|
||||
// begin status search
|
||||
activity.viewUrl(statusQuery)
|
||||
|
||||
// ensure that search is still ongoing
|
||||
assertTrue(activity.isSearching())
|
||||
|
||||
// return searchResults
|
||||
testScheduler.advanceTimeBy(100.milliseconds)
|
||||
|
||||
// ensure that the result of the status search was recorded
|
||||
// and the account search wasn't
|
||||
assertEquals(status.id, activity.statusId)
|
||||
assertEquals(null, activity.accountId)
|
||||
} finally {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBottomSheetActivity(api: MastodonApi) : BottomSheetActivity() {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ import com.keylesspalace.tusky.entity.Poll
|
|||
import com.keylesspalace.tusky.entity.PollOption
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -334,14 +334,14 @@ class FilterV1Test {
|
|||
PollOption(it, 0)
|
||||
},
|
||||
voted = false,
|
||||
ownVotes = null
|
||||
ownVotes = emptyList()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import com.keylesspalace.tusky.db.AccountEntity
|
|||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
|
|
@ -27,7 +30,6 @@ import org.robolectric.Robolectric
|
|||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.android.util.concurrent.BackgroundExecutor.runInBackground
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.Date
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -59,6 +61,11 @@ class MainActivityTest {
|
|||
WorkManagerTestInitHelper.initializeTestWorkManager(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
WorkManagerTestInitHelper.closeWorkDatabase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking notification of type FOLLOW shows notification tab`() {
|
||||
val intent = showNotification(Notification.Type.FOLLOW)
|
||||
|
|
@ -126,6 +133,8 @@ class MainActivityTest {
|
|||
on { activeAccount } doReturn accountEntity
|
||||
}
|
||||
activity.draftsAlert = mock {}
|
||||
activity.shareShortcutHelper = mock {}
|
||||
activity.externalScope = TestScope()
|
||||
activity.mastodonApi = mock {
|
||||
onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account)
|
||||
onBlocking { listAnnouncements(false) } doReturn NetworkResult.success(emptyList())
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import com.squareup.moshi.adapter
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
|
@ -37,7 +38,7 @@ class StatusComparisonTest {
|
|||
assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||
}
|
||||
|
||||
private val gson = Gson()
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Test
|
||||
fun `two equal status view data - should be equal`() {
|
||||
|
|
@ -90,6 +91,7 @@ class StatusComparisonTest {
|
|||
assertNotEquals(viewdata1, viewdata2)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun createStatus(
|
||||
id: String = "123456",
|
||||
content: String = """
|
||||
|
|
@ -201,6 +203,6 @@ class StatusComparisonTest {
|
|||
"poll": null
|
||||
}
|
||||
""".trimIndent()
|
||||
return gson.fromJson(statusJson, Status::class.java)
|
||||
return moshi.adapter<Status>().fromJson(statusJson)!!
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Looper.getMainLooper
|
||||
|
|
@ -23,8 +23,6 @@ import android.widget.EditText
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
|
|
@ -32,11 +30,20 @@ import com.keylesspalace.tusky.db.AppDatabase
|
|||
import com.keylesspalace.tusky.db.EmojisEntity
|
||||
import com.keylesspalace.tusky.db.InstanceDao
|
||||
import com.keylesspalace.tusky.db.InstanceInfoEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||
import com.keylesspalace.tusky.entity.InstanceV1
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
@ -44,6 +51,7 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
|
|
@ -51,7 +59,8 @@ import org.robolectric.Robolectric
|
|||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.fakes.RoboMenuItem
|
||||
import java.util.Locale
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/7/18.
|
||||
|
|
@ -87,8 +96,10 @@ class ComposeActivityTest {
|
|||
notificationVibration = true,
|
||||
notificationLight = true
|
||||
)
|
||||
private var instanceV1ResponseCallback: (() -> InstanceV1)? = null
|
||||
private var instanceResponseCallback: (() -> Instance)? = null
|
||||
private var composeOptions: ComposeActivity.ComposeOptions? = null
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun setupActivity() {
|
||||
|
|
@ -102,17 +113,27 @@ class ComposeActivityTest {
|
|||
apiMock = mock {
|
||||
onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList())
|
||||
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||
if (instance == null) {
|
||||
NetworkResult.failure(HttpException(Response.error<ResponseBody>(404, "Not found".toResponseBody())))
|
||||
} else {
|
||||
NetworkResult.success(instance)
|
||||
}
|
||||
}
|
||||
onBlocking { getInstanceV1() } doReturn instanceV1ResponseCallback?.invoke().let { instance ->
|
||||
if (instance == null) {
|
||||
NetworkResult.failure(Throwable())
|
||||
} else {
|
||||
NetworkResult.success(instance)
|
||||
}
|
||||
}
|
||||
onBlocking { search(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn NetworkResult.success(
|
||||
SearchResult(emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
}
|
||||
|
||||
val instanceDaoMock: InstanceDao = mock {
|
||||
onBlocking { getInstanceInfo(any()) } doReturn
|
||||
InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
|
||||
InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
|
||||
onBlocking { getEmojiInfo(any()) } doReturn
|
||||
EmojisEntity(instanceDomain, emptyList())
|
||||
}
|
||||
|
|
@ -121,7 +142,7 @@ class ComposeActivityTest {
|
|||
on { instanceDao() } doReturn instanceDaoMock
|
||||
}
|
||||
|
||||
val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock)
|
||||
val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock, CoroutineScope(SupervisorJob()))
|
||||
|
||||
val viewModel = ComposeViewModel(
|
||||
apiMock,
|
||||
|
|
@ -192,22 +213,13 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null) }
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null) }
|
||||
setupActivity()
|
||||
assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMaximumTootCharsIsPopulated_customLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum) }
|
||||
setupActivity()
|
||||
|
|
@ -215,10 +227,19 @@ class ComposeActivityTest {
|
|||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
|
|
@ -227,7 +248,7 @@ class ComposeActivityTest {
|
|||
@Test
|
||||
fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) }
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum * 2, activity.maximumTootCharacters)
|
||||
|
|
@ -237,7 +258,21 @@ class ComposeActivityTest {
|
|||
fun whenTextContainsNoUrl_everyCharacterIsCounted() {
|
||||
val content = "This is test content please ignore thx "
|
||||
insertSomeTextInContent(content)
|
||||
assertEquals(activity.calculateTextLength(), content.length)
|
||||
assertEquals(content.length, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsEmoji_emojisAreCountedAsOneCharacter() {
|
||||
val content = "Test 😜"
|
||||
insertSomeTextInContent(content)
|
||||
assertEquals(6, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsUrlWithEmoji_ellipsizedUrlIsCountedCorrectly() {
|
||||
val content = "https://🤪.com"
|
||||
insertSomeTextInContent(content)
|
||||
assertEquals(InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -245,7 +280,7 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL)
|
||||
assertEquals(additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -254,7 +289,7 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
|
||||
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -262,7 +297,7 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
|
||||
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -270,11 +305,23 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength)
|
||||
assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfigurationV1() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -283,11 +330,24 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
|
||||
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfigurationV1() {
|
||||
val shortUrl = "https://tusky.app"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -295,11 +355,23 @@ class ComposeActivityTest {
|
|||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
|
||||
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfigurationV1() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -478,6 +550,15 @@ class ComposeActivityTest {
|
|||
assertEquals(language, activity.selectedLanguage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sampleFriendicaInstanceResponseIsDeserializable() {
|
||||
// https://github.com/tuskyapp/Tusky/issues/4100
|
||||
instanceResponseCallback = { getSampleFriendicaInstance() }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(FRIENDICA_MAXIMUM, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
private fun clickUp() {
|
||||
val menuItem = RoboMenuItem(android.R.id.home)
|
||||
activity.onOptionsItemSelected(menuItem)
|
||||
|
|
@ -491,8 +572,33 @@ class ComposeActivityTest {
|
|||
activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text")
|
||||
}
|
||||
|
||||
private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance {
|
||||
private fun getInstanceWithCustomConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): Instance {
|
||||
return Instance(
|
||||
domain = "https://example.token",
|
||||
version = "2.6.3",
|
||||
configuration = getConfiguration(maximumStatusCharacters, charactersReservedPerUrl),
|
||||
pleroma = null,
|
||||
rules = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration {
|
||||
return Instance.Configuration(
|
||||
Instance.Configuration.Urls(),
|
||||
Instance.Configuration.Accounts(1),
|
||||
Instance.Configuration.Statuses(
|
||||
maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT,
|
||||
InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS,
|
||||
charactersReservedPerUrl ?: InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
||||
),
|
||||
Instance.Configuration.MediaAttachments(0, 0, 0, 0, 0),
|
||||
Instance.Configuration.Polls(0, 0, 0, 0),
|
||||
Instance.Configuration.Translation(false)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInstanceV1WithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): InstanceV1 {
|
||||
return InstanceV1(
|
||||
uri = "https://example.token",
|
||||
version = "2.6.3",
|
||||
maxTootChars = maximumLegacyTootCharacters,
|
||||
|
|
@ -516,4 +622,84 @@ class ComposeActivityTest {
|
|||
polls = null
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getSampleFriendicaInstance(): Instance {
|
||||
return moshi.adapter<Instance>().fromJson(sampleFriendicaResponse)!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FRIENDICA_MAXIMUM = 200000
|
||||
|
||||
// https://github.com/tuskyapp/Tusky/issues/4100
|
||||
private val sampleFriendicaResponse = """{
|
||||
"domain": "loma.ml",
|
||||
"title": "[ˈloma]",
|
||||
"version": "2.8.0 (compatible; Friendica 2023.09-rc)",
|
||||
"source_url": "https://git.friendi.ca/friendica/friendica",
|
||||
"description": "loma.ml ist eine Friendica Community im Fediverse auf der vorwiegend DE \uD83C\uDDE9\uD83C\uDDEA gesprochen wird. \\r\\nServer in Germany/EU \uD83C\uDDE9\uD83C\uDDEA \uD83C\uDDEA\uD83C\uDDFA. Open to all with fun in new. \\r\\nServer in Deutschland. Offen für alle mit Spaß an Neuen.",
|
||||
"usage": {
|
||||
"users": {
|
||||
"active_month": 125
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
"url": "https://loma.ml/ad/friendica-banner.jpg"
|
||||
},
|
||||
"languages": [
|
||||
"de"
|
||||
],
|
||||
"configuration": {
|
||||
"statuses": {
|
||||
"max_characters": $FRIENDICA_MAXIMUM
|
||||
},
|
||||
"media_attachments": {
|
||||
"supported_mime_types": {
|
||||
"image/jpeg": "jpg",
|
||||
"image/jpg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/gif": "gif"
|
||||
},
|
||||
"image_size_limit": 10485760
|
||||
}
|
||||
},
|
||||
"registrations": {
|
||||
"enabled": true,
|
||||
"approval_required": false
|
||||
},
|
||||
"contact": {
|
||||
"email": "anony@miz.ed",
|
||||
"account": {
|
||||
"id": "9632",
|
||||
"username": "webm",
|
||||
"acct": "webm",
|
||||
"display_name": "web m \uD83C\uDDEA\uD83C\uDDFA",
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": true,
|
||||
"group": false,
|
||||
"created_at": "2018-05-21T11:24:55.000Z",
|
||||
"note": "\uD83C\uDDE9\uD83C\uDDEA Über diesen Account werden Änderungen oder geplante Beeinträchtigungen angekündigt. Wenn du einen Account auf Loma.ml besitzt, dann solltest du dich mit mir verbinden.\uD83C\uDDEA\uD83C\uDDFA Changes or planned impairments are announced via this account. If you have an account on Loma.ml, you should connect to me.\uD83C\uDD98 Fallbackaccount @webm@joinfriendica.de",
|
||||
"url": "https://loma.ml/profile/webm",
|
||||
"avatar": "https://loma.ml/photo/contact/320/373ebf56355ac895a09cb99264485383?ts=1686417730",
|
||||
"avatar_static": "https://loma.ml/photo/contact/320/373ebf56355ac895a09cb99264485383?ts=1686417730&static=1",
|
||||
"header": "https://loma.ml/photo/header/373ebf56355ac895a09cb99264485383?ts=1686417730",
|
||||
"header_static": "https://loma.ml/photo/header/373ebf56355ac895a09cb99264485383?ts=1686417730&static=1",
|
||||
"followers_count": 23,
|
||||
"following_count": 25,
|
||||
"statuses_count": 15,
|
||||
"last_status_at": "2023-09-19T00:00:00.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
},
|
||||
"rules": [],
|
||||
"friendica": {
|
||||
"version": "2023.09-rc",
|
||||
"codename": "Giant Rhubarb",
|
||||
"db_version": 1539
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,8 @@
|
|||
* 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.components.compose.ComposeTokenizer
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import com.keylesspalace.tusky.components.compose.ComposeTokenizer
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -15,29 +15,31 @@
|
|||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import com.keylesspalace.tusky.SpanUtilsTest
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class StatusLengthTest(
|
||||
private val text: String,
|
||||
private val expectedLength: Int
|
||||
) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
arrayOf("", 0),
|
||||
arrayOf(" ", 1),
|
||||
arrayOf("123", 3),
|
||||
arrayOf("🫣", 1),
|
||||
// "@user@server" should be treated as "@user"
|
||||
arrayOf("123 @example@example.org", 12),
|
||||
// URLs under 23 chars are treated as 23 chars
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsPagingSourceTest {
|
||||
@Test
|
||||
fun `load() returns error message on HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{error: 'This is an error'}".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
val loadResult = pagingSource.load(loadingParams)
|
||||
|
||||
// Then
|
||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||
assertEquals(
|
||||
"HTTP 429: This is an error",
|
||||
(loadResult as PagingSource.LoadResult.Error).throwable.message
|
||||
)
|
||||
}
|
||||
|
||||
// As previous, but with `error_description` field as well.
|
||||
@Test
|
||||
fun `load() returns extended error message on HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{error: 'This is an error', error_description: 'Description of the error'}".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
val loadResult = pagingSource.load(loadingParams)
|
||||
|
||||
// Then
|
||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||
assertEquals(
|
||||
"HTTP 429: This is an error: Description of the error",
|
||||
(loadResult as PagingSource.LoadResult.Error).throwable.message
|
||||
)
|
||||
}
|
||||
|
||||
// As previous, but no error JSON, so expect default response
|
||||
@Test
|
||||
fun `load() returns default error message on empty HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{}".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
val loadResult = pagingSource.load(loadingParams)
|
||||
|
||||
// Then
|
||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||
assertEquals(
|
||||
"HTTP 429: no reason given",
|
||||
(loadResult as PagingSource.LoadResult.Error).throwable.message
|
||||
)
|
||||
}
|
||||
|
||||
// As previous, but malformed JSON, so expect response with enough information to troubleshoot
|
||||
@Test
|
||||
fun `load() returns useful error message on malformed HTTP error`() = runTest {
|
||||
// Given
|
||||
val jsonError = "{'malformedjson}".toResponseBody()
|
||||
val mockApi: MastodonApi = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(429, jsonError)
|
||||
onBlocking { notification(any()) } doReturn Response.error(429, jsonError)
|
||||
}
|
||||
|
||||
val filter = emptySet<Notification.Type>()
|
||||
val gson = Gson()
|
||||
val pagingSource = NotificationsPagingSource(mockApi, gson, filter)
|
||||
val loadingParams = PagingSource.LoadParams.Refresh("0", 5, false)
|
||||
|
||||
// When
|
||||
val loadResult = pagingSource.load(loadingParams)
|
||||
|
||||
// Then
|
||||
assertTrue(loadResult is PagingSource.LoadResult.Error)
|
||||
assertEquals(
|
||||
"HTTP 429: {'malformedjson} (com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated string at line 1 column 17 path \$.)",
|
||||
(loadResult as PagingSource.LoadResult.Error).throwable.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Looper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainCoroutineRule constructor(private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()) : TestWatcher() {
|
||||
override fun starting(description: Description) {
|
||||
super.starting(description)
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description) {
|
||||
super.finished(description)
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
abstract class NotificationsViewModelTestBase {
|
||||
protected lateinit var notificationsRepository: NotificationsRepository
|
||||
protected lateinit var sharedPreferencesMap: MutableMap<String, Boolean>
|
||||
protected lateinit var sharedPreferences: SharedPreferences
|
||||
protected lateinit var accountManager: AccountManager
|
||||
protected lateinit var timelineCases: TimelineCases
|
||||
protected lateinit var eventHub: EventHub
|
||||
protected lateinit var viewModel: NotificationsViewModel
|
||||
|
||||
/** Empty success response, for API calls that return one */
|
||||
protected var emptySuccess = Response.success("".toResponseBody())
|
||||
|
||||
/** Empty error response, for API calls that return one */
|
||||
protected var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody())
|
||||
|
||||
/** Exception to throw when testing errors */
|
||||
protected val httpException = HttpException(emptyError)
|
||||
|
||||
@get:Rule
|
||||
val mainCoroutineRule = MainCoroutineRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
|
||||
notificationsRepository = mock()
|
||||
|
||||
// Backing store for sharedPreferences, to allow mutation in tests
|
||||
sharedPreferencesMap = mutableMapOf(
|
||||
PrefKeys.ANIMATE_GIF_AVATARS to false,
|
||||
PrefKeys.ANIMATE_CUSTOM_EMOJIS to false,
|
||||
PrefKeys.ABSOLUTE_TIME_VIEW to false,
|
||||
PrefKeys.SHOW_BOT_OVERLAY to true,
|
||||
PrefKeys.USE_BLURHASH to true,
|
||||
PrefKeys.CONFIRM_REBLOGS to true,
|
||||
PrefKeys.CONFIRM_FAVOURITES to false,
|
||||
PrefKeys.WELLBEING_HIDE_STATS_POSTS to false,
|
||||
PrefKeys.SHOW_NOTIFICATIONS_FILTER to true,
|
||||
PrefKeys.FAB_HIDE to false
|
||||
)
|
||||
|
||||
// Any getBoolean() call looks for the result in sharedPreferencesMap
|
||||
sharedPreferences = mock {
|
||||
on { getBoolean(any(), any()) } doAnswer { sharedPreferencesMap[it.arguments[0]] }
|
||||
}
|
||||
|
||||
accountManager = mock {
|
||||
on { activeAccount } doReturn AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.test",
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true,
|
||||
notificationsFilter = "['follow']",
|
||||
mediaPreviewEnabled = true,
|
||||
alwaysShowSensitiveMedia = true,
|
||||
alwaysOpenSpoiler = true
|
||||
)
|
||||
}
|
||||
eventHub = EventHub()
|
||||
timelineCases = mock()
|
||||
|
||||
viewModel = NotificationsViewModel(
|
||||
notificationsRepository,
|
||||
sharedPreferences,
|
||||
accountManager,
|
||||
timelineCases,
|
||||
eventHub
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
/**
|
||||
* Verify that [ClearNotifications] is handled correctly on receipt:
|
||||
*
|
||||
* - Is the correct [UiSuccess] or [UiError] value emitted?
|
||||
* - Are the correct [NotificationsRepository] functions called, with the correct arguments?
|
||||
* This is only tested in the success case; if it passed there it must also
|
||||
* have passed in the error case.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestClearNotifications : NotificationsViewModelTestBase() {
|
||||
@Test
|
||||
fun `clearing notifications succeeds && invalidate the repository`() = runTest {
|
||||
// Given
|
||||
notificationsRepository.stub { onBlocking { clearNotifications() } doReturn emptySuccess }
|
||||
|
||||
// When
|
||||
viewModel.accept(FallibleUiAction.ClearNotifications)
|
||||
|
||||
// Then
|
||||
verify(notificationsRepository).clearNotifications()
|
||||
verify(notificationsRepository).invalidate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearing notifications fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
notificationsRepository.stub { onBlocking { clearNotifications() } doReturn emptyError }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(FallibleUiAction.ClearNotifications)
|
||||
|
||||
// Then
|
||||
assertThat(awaitItem()).isInstanceOf(UiError::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
/**
|
||||
* Verify that [ApplyFilter] is handled correctly on receipt:
|
||||
*
|
||||
* - Is the [UiState] updated correctly?
|
||||
* - Are the correct [AccountManager] functions called, with the correct arguments?
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestFilter : NotificationsViewModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `should load initial filter from active account`() = runTest {
|
||||
viewModel.uiState.test {
|
||||
assertThat(awaitItem().activeFilter)
|
||||
.containsExactlyElementsIn(setOf(Notification.Type.FOLLOW))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should save filter to active account && update state`() = runTest {
|
||||
viewModel.uiState.test {
|
||||
// When
|
||||
viewModel.accept(InfallibleUiAction.ApplyFilter(setOf(Notification.Type.REBLOG)))
|
||||
|
||||
// Then
|
||||
// - filter saved to active account
|
||||
argumentCaptor<AccountEntity>().apply {
|
||||
verify(accountManager).saveAccount(capture())
|
||||
assertThat(this.lastValue.notificationsFilter)
|
||||
.isEqualTo("[\"reblog\"]")
|
||||
}
|
||||
|
||||
// - filter updated in uiState
|
||||
assertThat(expectMostRecentItem().activeFilter)
|
||||
.containsExactlyElementsIn(setOf(Notification.Type.REBLOG))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.doThrow
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
/**
|
||||
* Verify that [NotificationAction] are handled correctly on receipt:
|
||||
*
|
||||
* - Is the correct [UiSuccess] or [UiError] value emitted?
|
||||
* - Is the correct [TimelineCases] function called, with the correct arguments?
|
||||
* This is only tested in the success case; if it passed there it must also
|
||||
* have passed in the error case.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestNotificationAction : NotificationsViewModelTestBase() {
|
||||
/** Dummy relationship */
|
||||
private val relationship = Relationship(
|
||||
// Nothing special about these values, it's just to have something to return
|
||||
"1234",
|
||||
following = true,
|
||||
followedBy = true,
|
||||
blocking = false,
|
||||
muting = false,
|
||||
mutingNotifications = false,
|
||||
requested = false,
|
||||
showingReblogs = false,
|
||||
subscribing = null,
|
||||
blockingDomain = false,
|
||||
note = null,
|
||||
notifying = null
|
||||
)
|
||||
|
||||
/** Action to accept a follow request */
|
||||
private val acceptAction = NotificationAction.AcceptFollowRequest("1234")
|
||||
|
||||
/** Action to reject a follow request */
|
||||
private val rejectAction = NotificationAction.RejectFollowRequest("1234")
|
||||
|
||||
@Test
|
||||
fun `accepting follow request succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub {
|
||||
onBlocking { acceptFollowRequest(any()) } doReturn Single.just(relationship)
|
||||
}
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(acceptAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(NotificationActionSuccess::class.java)
|
||||
assertThat((item as NotificationActionSuccess).action).isEqualTo(acceptAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
argumentCaptor<String>().apply {
|
||||
verify(timelineCases).acceptFollowRequest(capture())
|
||||
assertThat(this.lastValue).isEqualTo("1234")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accepting follow request fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { acceptFollowRequest(any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(acceptAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.AcceptFollowRequest::class.java)
|
||||
assertThat(item.action).isEqualTo(acceptAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejecting follow request succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn Single.just(relationship) }
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(rejectAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(NotificationActionSuccess::class.java)
|
||||
assertThat((item as NotificationActionSuccess).action).isEqualTo(rejectAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
argumentCaptor<String>().apply {
|
||||
verify(timelineCases).rejectFollowRequest(capture())
|
||||
assertThat(this.lastValue).isEqualTo("1234")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejecting follow request fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(rejectAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.RejectFollowRequest::class.java)
|
||||
assertThat(item.action).isEqualTo(rejectAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.FilterV1Test.Companion.mockStatus
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.doThrow
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
/**
|
||||
* Verify that [StatusAction] are handled correctly on receipt:
|
||||
*
|
||||
* - Is the correct [UiSuccess] or [UiError] value emitted?
|
||||
* - Is the correct [TimelineCases] function called, with the correct arguments?
|
||||
* This is only tested in the success case; if it passed there it must also
|
||||
* have passed in the error case.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestStatusAction : NotificationsViewModelTestBase() {
|
||||
private val status = mockStatus(pollOptions = listOf("Choice 1", "Choice 2", "Choice 3"))
|
||||
private val statusViewData = StatusViewData.Concrete(
|
||||
status = status,
|
||||
isExpanded = true,
|
||||
isShowingContent = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
|
||||
/** Action to bookmark a status */
|
||||
private val bookmarkAction = StatusAction.Bookmark(true, statusViewData)
|
||||
|
||||
/** Action to favourite a status */
|
||||
private val favouriteAction = StatusAction.Favourite(true, statusViewData)
|
||||
|
||||
/** Action to reblog a status */
|
||||
private val reblogAction = StatusAction.Reblog(true, statusViewData)
|
||||
|
||||
/** Action to vote in a poll */
|
||||
private val voteInPollAction = StatusAction.VoteInPoll(
|
||||
poll = status.poll!!,
|
||||
choices = listOf(1, 0, 0),
|
||||
statusViewData
|
||||
)
|
||||
|
||||
/** Captors for status ID and state arguments */
|
||||
private val id = argumentCaptor<String>()
|
||||
private val state = argumentCaptor<Boolean>()
|
||||
|
||||
@Test
|
||||
fun `bookmark succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { bookmark(any(), any()) } doReturn NetworkResult.success(status) }
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(bookmarkAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(StatusActionSuccess.Bookmark::class.java)
|
||||
assertThat((item as StatusActionSuccess).action).isEqualTo(bookmarkAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
verify(timelineCases).bookmark(id.capture(), state.capture())
|
||||
assertThat(id.firstValue).isEqualTo(statusViewData.status.id)
|
||||
assertThat(state.firstValue).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bookmark fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { bookmark(any(), any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(bookmarkAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.Bookmark::class.java)
|
||||
assertThat(item.action).isEqualTo(bookmarkAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `favourite succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub {
|
||||
onBlocking { favourite(any(), any()) } doReturn NetworkResult.success(status)
|
||||
}
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(favouriteAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(StatusActionSuccess.Favourite::class.java)
|
||||
assertThat((item as StatusActionSuccess).action).isEqualTo(favouriteAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
verify(timelineCases).favourite(id.capture(), state.capture())
|
||||
assertThat(id.firstValue).isEqualTo(statusViewData.status.id)
|
||||
assertThat(state.firstValue).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `favourite fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { favourite(any(), any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(favouriteAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.Favourite::class.java)
|
||||
assertThat(item.action).isEqualTo(favouriteAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reblog succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { reblog(any(), any()) } doReturn NetworkResult.success(status) }
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(reblogAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(StatusActionSuccess.Reblog::class.java)
|
||||
assertThat((item as StatusActionSuccess).action).isEqualTo(reblogAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
verify(timelineCases).reblog(id.capture(), state.capture())
|
||||
assertThat(id.firstValue).isEqualTo(statusViewData.status.id)
|
||||
assertThat(state.firstValue).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reblog fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { reblog(any(), any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(reblogAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.Reblog::class.java)
|
||||
assertThat(item.action).isEqualTo(reblogAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `voteinpoll succeeds && emits UiSuccess`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub {
|
||||
onBlocking { voteInPoll(any(), any(), any()) } doReturn NetworkResult.success(status.poll!!)
|
||||
}
|
||||
|
||||
viewModel.uiSuccess.test {
|
||||
// When
|
||||
viewModel.accept(voteInPollAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(StatusActionSuccess.VoteInPoll::class.java)
|
||||
assertThat((item as StatusActionSuccess).action).isEqualTo(voteInPollAction)
|
||||
}
|
||||
|
||||
// Then
|
||||
val pollId = argumentCaptor<String>()
|
||||
val choices = argumentCaptor<List<Int>>()
|
||||
verify(timelineCases).voteInPoll(id.capture(), pollId.capture(), choices.capture())
|
||||
assertThat(id.firstValue).isEqualTo(statusViewData.status.id)
|
||||
assertThat(pollId.firstValue).isEqualTo(status.poll!!.id)
|
||||
assertThat(choices.firstValue).isEqualTo(voteInPollAction.choices)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `voteinpoll fails && emits UiError`() = runTest {
|
||||
// Given
|
||||
timelineCases.stub { onBlocking { voteInPoll(any(), any(), any()) } doThrow httpException }
|
||||
|
||||
viewModel.uiError.test {
|
||||
// When
|
||||
viewModel.accept(voteInPollAction)
|
||||
|
||||
// Then
|
||||
val item = awaitItem()
|
||||
assertThat(item).isInstanceOf(UiError.VoteInPoll::class.java)
|
||||
assertThat(item.action).isEqualTo(voteInPollAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Verify that [StatusDisplayOptions] are handled correctly.
|
||||
*
|
||||
* - Is the initial value taken from values in sharedPreferences and account?
|
||||
* - Does the make() function correctly use an updated preference?
|
||||
* - Is the correct update emitted when a relevant preference changes?
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestStatusDisplayOptions : NotificationsViewModelTestBase() {
|
||||
|
||||
private val defaultStatusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = false,
|
||||
mediaPreviewEnabled = true, // setting in NotificationsViewModelTestBase
|
||||
useAbsoluteTime = false,
|
||||
showBotOverlay = true,
|
||||
useBlurhash = true,
|
||||
cardViewMode = CardViewMode.NONE,
|
||||
confirmReblogs = true,
|
||||
confirmFavourites = false,
|
||||
hideStats = false,
|
||||
animateEmojis = false,
|
||||
showStatsInline = false,
|
||||
showSensitiveMedia = true, // setting in NotificationsViewModelTestBase
|
||||
openSpoiler = true // setting in NotificationsViewModelTestBase
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `initial settings are from sharedPreferences and activeAccount`() = runTest {
|
||||
viewModel.statusDisplayOptions.test {
|
||||
val item = awaitItem()
|
||||
assertThat(item).isEqualTo(defaultStatusDisplayOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `make() uses updated preference`() = runTest {
|
||||
// Prior, should be false
|
||||
assertThat(defaultStatusDisplayOptions.animateAvatars).isFalse()
|
||||
|
||||
// Given; just a change to one preferences
|
||||
sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true
|
||||
|
||||
// When
|
||||
val updatedOptions = defaultStatusDisplayOptions.make(
|
||||
sharedPreferences,
|
||||
PrefKeys.ANIMATE_GIF_AVATARS,
|
||||
accountManager.activeAccount!!
|
||||
)
|
||||
|
||||
// Then, should be true
|
||||
assertThat(updatedOptions.animateAvatars).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PreferenceChangedEvent emits new StatusDisplayOptions`() = runTest {
|
||||
// Prior, should be false
|
||||
viewModel.statusDisplayOptions.test {
|
||||
val item = expectMostRecentItem()
|
||||
assertThat(item.animateAvatars).isFalse()
|
||||
}
|
||||
|
||||
// Given
|
||||
sharedPreferencesMap[PrefKeys.ANIMATE_GIF_AVATARS] = true
|
||||
|
||||
// When
|
||||
eventHub.dispatch(PreferenceChangedEvent(PrefKeys.ANIMATE_GIF_AVATARS))
|
||||
|
||||
// Then, should be true
|
||||
viewModel.statusDisplayOptions.test {
|
||||
val item = expectMostRecentItem()
|
||||
assertThat(item.animateAvatars).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Verify that [UiState] is handled correctly.
|
||||
*
|
||||
* - Is the initial value taken from values in sharedPreferences and account?
|
||||
* - Is the correct update emitted when a relevant preference changes?
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestUiState : NotificationsViewModelTestBase() {
|
||||
|
||||
private val initialUiState = UiState(
|
||||
activeFilter = setOf(Notification.Type.FOLLOW),
|
||||
showFilterOptions = true,
|
||||
showFabWhileScrolling = true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `should load initial filter from active account`() = runTest {
|
||||
viewModel.uiState.test {
|
||||
assertThat(expectMostRecentItem()).isEqualTo(initialUiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `showFabWhileScrolling depends on FAB_HIDE preference`() = runTest {
|
||||
// Prior
|
||||
viewModel.uiState.test {
|
||||
assertThat(expectMostRecentItem().showFabWhileScrolling).isTrue()
|
||||
}
|
||||
|
||||
// Given
|
||||
sharedPreferencesMap[PrefKeys.FAB_HIDE] = true
|
||||
|
||||
// When
|
||||
eventHub.dispatch(PreferenceChangedEvent(PrefKeys.FAB_HIDE))
|
||||
|
||||
// Then
|
||||
viewModel.uiState.test {
|
||||
assertThat(expectMostRecentItem().showFabWhileScrolling).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `showFilterOptions depends on SHOW_NOTIFICATIONS_FILTER preference`() = runTest {
|
||||
// Prior
|
||||
viewModel.uiState.test {
|
||||
assertThat(expectMostRecentItem().showFilterOptions).isTrue()
|
||||
}
|
||||
|
||||
// Given
|
||||
sharedPreferencesMap[PrefKeys.SHOW_NOTIFICATIONS_FILTER] = false
|
||||
|
||||
// When
|
||||
eventHub.dispatch(PreferenceChangedEvent(PrefKeys.SHOW_NOTIFICATIONS_FILTER))
|
||||
|
||||
// Then
|
||||
viewModel.uiState.test {
|
||||
assertThat(expectMostRecentItem().showFilterOptions).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* 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.components.notifications
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModelTestVisibleId : NotificationsViewModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `should save notification ID to active account`() = runTest {
|
||||
argumentCaptor<AccountEntity>().apply {
|
||||
// When
|
||||
viewModel.accept(InfallibleUiAction.SaveVisibleId("1234"))
|
||||
|
||||
// Then
|
||||
verify(accountManager).saveAccount(capture())
|
||||
assertThat(this.lastValue.lastNotificationId)
|
||||
.isEqualTo("1234")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,14 @@ import androidx.paging.RemoteMediator
|
|||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
|
|
@ -35,7 +36,6 @@ import org.robolectric.Shadows.shadowOf
|
|||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -54,6 +54,8 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setup() {
|
||||
|
|
@ -61,7 +63,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(Gson()))
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +82,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
|
|
@ -99,7 +101,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
|
|
@ -115,7 +117,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
accountManager = accountManager,
|
||||
api = mock(),
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -166,7 +168,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -229,7 +231,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -289,7 +291,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -334,7 +336,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -385,7 +387,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -441,7 +443,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -493,7 +495,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
},
|
||||
db = db,
|
||||
gson = Gson()
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import androidx.paging.RemoteMediator
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineRemoteMediator
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Headers
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
|
|
@ -27,7 +29,6 @@ import org.mockito.kotlin.verify
|
|||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
@Config(sdk = [29])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -382,6 +383,59 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
assertEquals(newStatusData, statuses)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not append duplicates for trending statuses`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "3"
|
||||
on { kind } doReturn TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
|
||||
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
),
|
||||
Headers.headersOf(
|
||||
"Link",
|
||||
"<https://mastodon.example/api/v1/trends/statuses?offset=5>; rel=\"next\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = statuses,
|
||||
prevKey = null,
|
||||
nextKey = "3"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
)
|
||||
verify(timelineViewModel).nextKey = "5"
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
assertEquals(newStatusData, statuses)
|
||||
}
|
||||
|
||||
private fun state(pages: List<PagingSource.LoadResult.Page<String, StatusViewData>> = emptyList()) = PagingState(
|
||||
pages = pages,
|
||||
anchorPosition = null,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
|
@ -54,7 +54,7 @@ fun mockStatus(
|
|||
poll = null,
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
)
|
||||
|
||||
fun mockStatusViewData(
|
||||
|
|
@ -91,19 +91,19 @@ fun mockStatusEntityWithAccount(
|
|||
expanded: Boolean = false
|
||||
): TimelineStatusWithAccount {
|
||||
val mockedStatus = mockStatus(id)
|
||||
val gson = Gson()
|
||||
val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
return TimelineStatusWithAccount(
|
||||
status = mockedStatus.toEntity(
|
||||
timelineUserId = userId,
|
||||
gson = gson,
|
||||
moshi = moshi,
|
||||
expanded = expanded,
|
||||
contentShowing = false,
|
||||
contentCollapsed = true
|
||||
),
|
||||
account = mockedStatus.account.toEntity(
|
||||
accountId = userId,
|
||||
gson = gson
|
||||
moshi = moshi
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,20 @@ import androidx.room.Room
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.appstore.BookmarkEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
||||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
||||
import com.keylesspalace.tusky.components.timeline.mockStatus
|
||||
import com.keylesspalace.tusky.components.timeline.mockStatusViewData
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.StatusContext
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
|
|
@ -34,7 +33,6 @@ import org.mockito.kotlin.mock
|
|||
import org.mockito.kotlin.stub
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.IOException
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -46,6 +44,7 @@ class ViewThreadViewModelTest {
|
|||
private lateinit var db: AppDatabase
|
||||
|
||||
private val threadId = "1234"
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
/**
|
||||
* Execute each task synchronously.
|
||||
|
|
@ -79,7 +78,9 @@ class ViewThreadViewModelTest {
|
|||
fun setup() {
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
api = mock()
|
||||
api = mock {
|
||||
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
|
||||
}
|
||||
eventHub = EventHub()
|
||||
val filterModel = FilterModel()
|
||||
val timelineCases = TimelineCases(api, eventHub)
|
||||
|
|
@ -95,12 +96,11 @@ class ViewThreadViewModelTest {
|
|||
}
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(Gson()))
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
val gson = Gson()
|
||||
viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager, db, gson)
|
||||
viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager, db, moshi)
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
@ -216,13 +216,13 @@ class ViewThreadViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `should handle favorite event`() {
|
||||
fun `should handle status changed event`() {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
eventHub.dispatch(FavoriteEvent(statusId = "1", false))
|
||||
eventHub.dispatch(StatusChangedEvent(mockStatus(id = "1", spoilerText = "Test", favourited = false)))
|
||||
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
|
|
@ -239,54 +239,6 @@ class ViewThreadViewModelTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle reblog event`() {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
eventHub.dispatch(ReblogEvent(statusId = "2", true))
|
||||
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", reblogged = true),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle bookmark event`() {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
eventHub.dispatch(BookmarkEvent(statusId = "3", false))
|
||||
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", bookmarked = false)
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should remove status`() {
|
||||
mockSuccessResponses()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import androidx.paging.PagingSource
|
|||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
|
|
@ -23,11 +23,13 @@ class TimelineDaoTest {
|
|||
private lateinit var timelineDao: TimelineDao
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(Gson()))
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
timelineDao = db.timelineDao()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
package com.keylesspalace.tusky.json
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GuardedBooleanAdapterTest {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class GuardedAdapterTest {
|
||||
|
||||
private val gson = Gson()
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(GuardedAdapter.ANNOTATION_FACTORY)
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() {
|
||||
|
|
@ -45,7 +49,7 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java)
|
||||
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +89,7 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java)
|
||||
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +128,7 @@ class GuardedBooleanAdapterTest {
|
|||
note = "Hi",
|
||||
notifying = false
|
||||
),
|
||||
gson.fromJson(jsonInput, Relationship::class.java)
|
||||
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import app.cash.turbine.test
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PinEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -19,7 +20,6 @@ import org.mockito.kotlin.stub
|
|||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -39,15 +39,17 @@ class TimelineCasesTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `pin success emits PinEvent`() {
|
||||
fun `pin success emits StatusChangedEvent`() {
|
||||
val pinnedStatus = mockStatus(pinned = true)
|
||||
|
||||
api.stub {
|
||||
onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(mockStatus(pinned = true))
|
||||
onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(pinnedStatus)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
eventHub.events.test {
|
||||
timelineCases.pin(statusId, true)
|
||||
assertEquals(PinEvent(statusId, true), awaitItem())
|
||||
assertEquals(StatusChangedEvent(pinnedStatus), awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +104,7 @@ class TimelineCasesTest {
|
|||
poll = null,
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = null
|
||||
filtered = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,35 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import java.util.*
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
|
||||
class AbsoluteTimeFormatterTest {
|
||||
companion object {
|
||||
/** Default locale before this test started */
|
||||
private lateinit var locale: Locale
|
||||
|
||||
/**
|
||||
* Ensure the Locale is ENGLISH so that tests against literal strings like
|
||||
* "Apr" later, even if the test host's locale is e.g. FRENCH which would
|
||||
* normally report "avr.".
|
||||
*/
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
locale = Locale.getDefault()
|
||||
Locale.setDefault(Locale.ENGLISH)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun afterClass() {
|
||||
Locale.setDefault(locale)
|
||||
}
|
||||
}
|
||||
|
||||
private val formatter = AbsoluteTimeFormatter(TimeZone.getTimeZone("UTC"))
|
||||
private val now = Date.from(Instant.parse("2022-04-11T00:00:00.00Z"))
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ package com.keylesspalace.tusky.util
|
|||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
||||
class FlowExtensionsTest {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import android.widget.TextView
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.R
|
||||
|
|
@ -33,8 +33,8 @@ class LinkHelperTest {
|
|||
HashTag("mastodev", "https://example.com/Tags/mastodev")
|
||||
)
|
||||
|
||||
private val context: Context
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val textView: TextView
|
||||
get() = TextView(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
|
||||
@Test
|
||||
fun whenSettingClickableText_mentionUrlsArePreserved() {
|
||||
|
|
@ -168,8 +168,8 @@ class LinkHelperTest {
|
|||
content.append(displayedContent, URLSpan(maliciousUrl), 0)
|
||||
val oldContent = content.toString()
|
||||
Assert.assertEquals(
|
||||
context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
markupHiddenUrls(context, content).toString()
|
||||
textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
markupHiddenUrls(textView, content).toString()
|
||||
)
|
||||
Assert.assertEquals(oldContent, content.toString())
|
||||
}
|
||||
|
|
@ -182,8 +182,8 @@ class LinkHelperTest {
|
|||
val content = SpannableStringBuilder()
|
||||
content.append(displayedContent, URLSpan(maliciousUrl), 0)
|
||||
Assert.assertEquals(
|
||||
context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
markupHiddenUrls(context, content).toString()
|
||||
textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
markupHiddenUrls(textView, content).toString()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -196,9 +196,9 @@ class LinkHelperTest {
|
|||
content.append(displayedContent, URLSpan("https://$domain/foo/bar"), 0)
|
||||
}
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, content)
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
for (domain in domains) {
|
||||
Assert.assertTrue(markedUpContent.contains(context.getString(R.string.url_domain_notifier, displayedContent, domain)))
|
||||
Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, displayedContent, domain)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ class LinkHelperTest {
|
|||
.append("$domain/", URLSpan("https://$domain"), 0)
|
||||
.append("$domain/", URLSpan("https://www.$domain"), 0)
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, content)
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
Assert.assertFalse(markedUpContent.contains("🔗"))
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +228,7 @@ class LinkHelperTest {
|
|||
.append("Some Place | https://some.place/", URLSpan("https://some.place/"), 0)
|
||||
.append("Some Place https://some.place/path", URLSpan("https://some.place/path"), 0)
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, content)
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
Assert.assertFalse(markedUpContent.contains("🔗"))
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ class LinkHelperTest {
|
|||
.append("Another Place | https://another.place/", URLSpan("https://some.place/"), 0)
|
||||
.append("Another Place https://another.place/path", URLSpan("https://some.place/path"), 0)
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, content)
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
val asserts = listOf(
|
||||
"Another Place: another.place",
|
||||
"Another Place: another.place/",
|
||||
|
|
@ -250,7 +250,7 @@ class LinkHelperTest {
|
|||
"Another Place https://another.place/path"
|
||||
)
|
||||
asserts.forEach {
|
||||
Assert.assertTrue(markedUpContent.contains(context.getString(R.string.url_domain_notifier, it, "some.place")))
|
||||
Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, it, "some.place")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ class LinkHelperTest {
|
|||
builder.append(" ")
|
||||
}
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, builder)
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (mention in mentions) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ class LinkHelperTest {
|
|||
builder.append(" ")
|
||||
}
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, builder)
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (mention in mentions) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
}
|
||||
|
|
@ -290,7 +290,7 @@ class LinkHelperTest {
|
|||
builder.append(" ")
|
||||
}
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, builder)
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (tag in tags) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
}
|
||||
|
|
@ -304,7 +304,7 @@ class LinkHelperTest {
|
|||
builder.append(" ")
|
||||
}
|
||||
|
||||
val markedUpContent = markupHiddenUrls(context, builder)
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (tag in tags) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
}
|
||||
|
|
@ -348,7 +348,6 @@ class LinkHelperTest {
|
|||
arrayOf("https://pleroma.foo.bar/users/", false),
|
||||
arrayOf("https://pleroma.foo.bar/users/meow/", false),
|
||||
arrayOf("https://pleroma.foo.bar/users/@meow", false),
|
||||
arrayOf("https://pleroma.foo.bar/user/2345", false),
|
||||
arrayOf("https://pleroma.foo.bar/notices/123456", false),
|
||||
arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
|
||||
arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
|
||||
|
|
@ -367,7 +366,9 @@ class LinkHelperTest {
|
|||
arrayOf("https://pixelfed.social/connyduck", true),
|
||||
arrayOf("https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2", true),
|
||||
arrayOf("https://gts.foo.bar/@goblin", true),
|
||||
arrayOf("https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5", true)
|
||||
arrayOf("https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5", true),
|
||||
arrayOf("https://bookwyrm.foo.bar/user/User", true),
|
||||
arrayOf("https://bookwyrm.foo.bar/user/User/comment/123456", true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import java.util.Locale
|
||||
import kotlin.math.pow
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.util.Locale
|
||||
import kotlin.math.pow
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class NumberUtilsTest(private val input: Long, private val want: String) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue