Ignore "@instance..." part of username when computing status length (#3392)
* Move compose.* tests to own namespace
* Ignore "@instance..." part of username when computing status length
In a status with a mention ("@foo@example.org") only the "@foo" part should
be included in the calculated status length. It wasn't, so the app was
prevening people from posting statuses that should have been allowed.
Fix this.
- Lift the length calculation code in to a separate static function (easier
and faster to test)
- Add a `MentionSpan` type, to reuse existing code for detecting mentions
- Fix a bug in `FakeSpannable.getSpans()` (it was returning the outer type,
not the wrapped inner span)
- Add additional fast tests
The tests made sense under the `components.compose.ComposeActivity` package,
so I also created that and moved the existing ComposeActivity tests there.
Fixes https://github.com/tuskyapp/Tusky/issues/3339
* Static import assertEquals
This commit is contained in:
parent
f309c7750f
commit
6dfdaec425
8 changed files with 158 additions and 23 deletions
|
|
@ -0,0 +1,519 @@
|
|||
/*
|
||||
* Copyright 2018 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.compose.ComposeActivity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Looper.getMainLooper
|
||||
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
|
||||
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.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.fakes.RoboMenuItem
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/7/18.
|
||||
*/
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ComposeActivityTest {
|
||||
private lateinit var activity: ComposeActivity
|
||||
private lateinit var accountManagerMock: AccountManager
|
||||
private lateinit var apiMock: MastodonApi
|
||||
|
||||
private val instanceDomain = "example.domain"
|
||||
|
||||
private val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = instanceDomain,
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true,
|
||||
accountId = "1",
|
||||
username = "username",
|
||||
displayName = "Display Name",
|
||||
profilePictureUrl = "",
|
||||
notificationsEnabled = true,
|
||||
notificationsMentioned = true,
|
||||
notificationsFollowed = true,
|
||||
notificationsFollowRequested = false,
|
||||
notificationsReblogged = true,
|
||||
notificationsFavorited = true,
|
||||
notificationSound = true,
|
||||
notificationVibration = true,
|
||||
notificationLight = true
|
||||
)
|
||||
private var instanceResponseCallback: (() -> Instance)? = null
|
||||
private var composeOptions: ComposeActivity.ComposeOptions? = null
|
||||
|
||||
@Before
|
||||
fun setupActivity() {
|
||||
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
|
||||
activity = controller.get()
|
||||
|
||||
accountManagerMock = mock {
|
||||
on { activeAccount } doReturn account
|
||||
}
|
||||
|
||||
apiMock = mock {
|
||||
onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList())
|
||||
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||
if (instance == null) {
|
||||
NetworkResult.failure(Throwable())
|
||||
} else {
|
||||
NetworkResult.success(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val instanceDaoMock: InstanceDao = mock {
|
||||
onBlocking { getInstanceInfo(any()) } doReturn
|
||||
InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
|
||||
onBlocking { getEmojiInfo(any()) } doReturn
|
||||
EmojisEntity(instanceDomain, emptyList())
|
||||
}
|
||||
|
||||
val dbMock: AppDatabase = mock {
|
||||
on { instanceDao() } doReturn instanceDaoMock
|
||||
}
|
||||
|
||||
val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock)
|
||||
|
||||
val viewModel = ComposeViewModel(
|
||||
apiMock,
|
||||
accountManagerMock,
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
instanceInfoRepo
|
||||
)
|
||||
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
|
||||
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
||||
}
|
||||
|
||||
val viewModelFactoryMock: ViewModelFactory = mock {
|
||||
on { create(eq(ComposeViewModel::class.java), any()) } doReturn viewModel
|
||||
}
|
||||
|
||||
activity.accountManager = accountManagerMock
|
||||
activity.viewModelFactory = viewModelFactoryMock
|
||||
|
||||
controller.create().start()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenCloseButtonPressedAndEmpty_finish() {
|
||||
clickUp()
|
||||
assertTrue(activity.isFinishing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenCloseButtonPressedNotEmpty_notFinish() {
|
||||
insertSomeTextInContent()
|
||||
clickUp()
|
||||
assertFalse(activity.isFinishing)
|
||||
// We would like to check for dialog but Robolectric doesn't work with AppCompat v7 yet
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenModifiedInitialState_andCloseButtonPressed_notFinish() {
|
||||
composeOptions = ComposeActivity.ComposeOptions(modifiedInitialState = true)
|
||||
setupActivity()
|
||||
clickUp()
|
||||
assertFalse(activity.isFinishing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenBackButtonPressedAndEmpty_finish() {
|
||||
clickBack()
|
||||
assertTrue(activity.isFinishing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenBackButtonPressedNotEmpty_notFinish() {
|
||||
insertSomeTextInContent()
|
||||
clickBack()
|
||||
assertFalse(activity.isFinishing)
|
||||
// We would like to check for dialog but Robolectric doesn't work with AppCompat v7 yet
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenModifiedInitialState_andBackButtonPressed_notFinish() {
|
||||
composeOptions = ComposeActivity.ComposeOptions(modifiedInitialState = true)
|
||||
setupActivity()
|
||||
clickBack()
|
||||
assertFalse(activity.isFinishing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(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()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() {
|
||||
val customMaximum = 1000
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
assertEquals(customMaximum * 2, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsNoUrl_everyCharacterIsCounted() {
|
||||
val content = "This is test content please ignore thx "
|
||||
insertSomeTextInContent(content)
|
||||
assertEquals(activity.calculateTextLength(), content.length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted() {
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized() {
|
||||
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: "
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized() {
|
||||
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))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfiguration() {
|
||||
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)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfiguration() {
|
||||
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
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfiguration() {
|
||||
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)) }
|
||||
setupActivity()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionIsEmpty_specialTextIsInsertedAtCaret() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
editor.setText("Some text")
|
||||
|
||||
for (caretIndex in listOf(9, 1, 0)) {
|
||||
editor.setSelection(caretIndex)
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
// Text should be inserted at caret
|
||||
assertEquals("Unexpected value at $caretIndex", insertText, editor.text.substring(caretIndex, caretIndex + insertText.length))
|
||||
|
||||
// Caret should be placed after inserted text
|
||||
assertEquals(caretIndex + insertText.length, editor.selectionStart)
|
||||
assertEquals(caretIndex + insertText.length, editor.selectionEnd)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionDoesNotIncludeWordBreak_noSpecialTextIsInserted() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "Some text"
|
||||
val selectionStart = 1
|
||||
val selectionEnd = 4
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // "ome"
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text and selection should be unmodified
|
||||
assertEquals(originalText, editor.text.toString())
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionIncludesWordBreaks_startsOfAllWordsArePrepended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "one two three four"
|
||||
val selectionStart = 2
|
||||
val originalSelectionEnd = 15
|
||||
val modifiedSelectionEnd = 18
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, originalSelectionEnd) // "e two three f"
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// text should be inserted at word starts inside selection
|
||||
assertEquals("one #two #three #four", editor.text.toString())
|
||||
|
||||
// selection should be expanded accordingly
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(modifiedSelectionEnd, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionIncludesEnd_textIsNotAppended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "Some text"
|
||||
val selectionStart = 7
|
||||
val selectionEnd = 9
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // "xt"
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text and selection should be unmodified
|
||||
assertEquals(originalText, editor.text.toString())
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionIncludesStartAndStartIsAWord_textIsPrepended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "Some text"
|
||||
val selectionStart = 0
|
||||
val selectionEnd = 3
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // "Som"
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text should be inserted at beginning
|
||||
assert(editor.text.startsWith(insertText))
|
||||
|
||||
// selection should be expanded accordingly
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionIncludesStartAndStartIsNotAWord_textIsNotPrepended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = " Some text"
|
||||
val selectionStart = 0
|
||||
val selectionEnd = 1
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // " "
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text and selection should be unmodified
|
||||
assertEquals(originalText, editor.text.toString())
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionBeginsAtWordStart_textIsPrepended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "Some text"
|
||||
val selectionStart = 5
|
||||
val selectionEnd = 9
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // "text"
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text is prepended
|
||||
assertEquals("Some #text", editor.text.toString())
|
||||
|
||||
// Selection is expanded accordingly
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSelectionEndsAtWordStart_textIsAppended() {
|
||||
val editor = activity.findViewById<EditText>(R.id.composeEditField)
|
||||
val insertText = "#"
|
||||
val originalText = "Some text"
|
||||
val selectionStart = 1
|
||||
val selectionEnd = 5
|
||||
editor.setText(originalText)
|
||||
editor.setSelection(selectionStart, selectionEnd) // "ome "
|
||||
activity.prependSelectedWordsWith(insertText)
|
||||
|
||||
// Text is prepended
|
||||
assertEquals("Some #text", editor.text.toString())
|
||||
|
||||
// Selection is expanded accordingly
|
||||
assertEquals(selectionStart, editor.selectionStart)
|
||||
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenNoLanguageIsGiven_defaultLanguageIsSelected() {
|
||||
assertEquals(Locale.getDefault().language, activity.selectedLanguage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun languageGivenInComposeOptionsIsRespected() {
|
||||
val language = "no"
|
||||
composeOptions = ComposeActivity.ComposeOptions(language = language)
|
||||
setupActivity()
|
||||
assertEquals(language, activity.selectedLanguage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun modernLanguageCodeIsUsed() {
|
||||
// https://github.com/tuskyapp/Tusky/issues/2903
|
||||
// "ji" was deprecated in favor of "yi"
|
||||
composeOptions = ComposeActivity.ComposeOptions(language = "ji")
|
||||
setupActivity()
|
||||
assertEquals("yi", activity.selectedLanguage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownLanguageGivenInComposeOptionsIsRespected() {
|
||||
val language = "zzz"
|
||||
composeOptions = ComposeActivity.ComposeOptions(language = language)
|
||||
setupActivity()
|
||||
assertEquals(language, activity.selectedLanguage)
|
||||
}
|
||||
|
||||
private fun clickUp() {
|
||||
val menuItem = RoboMenuItem(android.R.id.home)
|
||||
activity.onOptionsItemSelected(menuItem)
|
||||
}
|
||||
|
||||
private fun clickBack() {
|
||||
activity.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
private fun insertSomeTextInContent(text: String? = null) {
|
||||
activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text")
|
||||
}
|
||||
|
||||
private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance {
|
||||
return Instance(
|
||||
uri = "https://example.token",
|
||||
version = "2.6.3",
|
||||
maxTootChars = maximumLegacyTootCharacters,
|
||||
pollConfiguration = null,
|
||||
configuration = configuration,
|
||||
maxMediaAttachments = null,
|
||||
pleroma = null,
|
||||
uploadLimit = null,
|
||||
rules = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
|
||||
return InstanceConfiguration(
|
||||
statuses = StatusConfiguration(
|
||||
maxCharacters = maximumStatusCharacters,
|
||||
maxMediaAttachments = null,
|
||||
charactersReservedPerUrl = charactersReservedPerUrl
|
||||
),
|
||||
mediaAttachments = null,
|
||||
polls = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.compose.ComposeActivity
|
||||
|
||||
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
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class StatusLengthTest(
|
||||
private val text: String,
|
||||
private val expectedLength: Int
|
||||
) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
arrayOf("", 0),
|
||||
arrayOf(" ", 1),
|
||||
arrayOf("123", 3),
|
||||
// "@user@server" should be treated as "@user"
|
||||
arrayOf("123 @example@example.org", 12),
|
||||
// URLs under 23 chars are treated as 23 chars
|
||||
arrayOf("123 http://example.url", 27),
|
||||
// URLs over 23 chars are treated as 23 chars
|
||||
arrayOf("123 http://urlthatislongerthan23characters.example.org", 27),
|
||||
// Short hashtags are treated as is
|
||||
arrayOf("123 #basictag", 13),
|
||||
// Long hashtags are *also* treated as is (not treated as 23, like URLs)
|
||||
arrayOf("123 #atagthatislongerthan23characters", 37)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusLength_matchesExpectations() {
|
||||
val spannedText = SpanUtilsTest.FakeSpannable(text)
|
||||
highlightSpans(spannedText, 0)
|
||||
|
||||
assertEquals(
|
||||
expectedLength,
|
||||
ComposeActivity.statusLength(spannedText, null, 23)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusLength_withCwText_matchesExpectations() {
|
||||
val spannedText = SpanUtilsTest.FakeSpannable(text)
|
||||
highlightSpans(spannedText, 0)
|
||||
|
||||
val cwText = SpanUtilsTest.FakeSpannable(
|
||||
"a @example@example.org #hashtagmention and http://example.org URL"
|
||||
)
|
||||
assertEquals(
|
||||
expectedLength + cwText.length,
|
||||
ComposeActivity.statusLength(spannedText, cwText, 23)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/* Copyright 2018 Levi Bard
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.compose.ComposeTokenizer
|
||||
|
||||
import com.keylesspalace.tusky.components.compose.ComposeTokenizer
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class ComposeTokenizerTest(
|
||||
private val text: CharSequence,
|
||||
private val expectedStartIndex: Int,
|
||||
private val expectedEndIndex: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
arrayOf("@mention", 0, 8),
|
||||
arrayOf("@ment10n", 0, 8),
|
||||
arrayOf("@ment10n_", 0, 9),
|
||||
arrayOf("@ment10n_n", 0, 10),
|
||||
arrayOf("@ment10n_9", 0, 10),
|
||||
arrayOf(" @mention", 1, 9),
|
||||
arrayOf(" @ment10n", 1, 9),
|
||||
arrayOf(" @ment10n_", 1, 10),
|
||||
arrayOf(" @ment10n_ @", 11, 12),
|
||||
arrayOf(" @ment10n_ @ment20n", 11, 19),
|
||||
arrayOf(" @ment10n_ @ment20n_", 11, 20),
|
||||
arrayOf(" @ment10n_ @ment20n_n", 11, 21),
|
||||
arrayOf(" @ment10n_ @ment20n_9", 11, 21),
|
||||
arrayOf(" @ment10n-", 1, 10),
|
||||
arrayOf(" @ment10n- @", 11, 12),
|
||||
arrayOf(" @ment10n- @ment20n", 11, 19),
|
||||
arrayOf(" @ment10n- @ment20n-", 11, 20),
|
||||
arrayOf(" @ment10n- @ment20n-n", 11, 21),
|
||||
arrayOf(" @ment10n- @ment20n-9", 11, 21),
|
||||
arrayOf("@ment10n@l0calhost", 0, 18),
|
||||
arrayOf(" @ment10n@l0calhost", 1, 19),
|
||||
arrayOf(" @ment10n_@l0calhost", 1, 20),
|
||||
arrayOf(" @ment10n-@l0calhost", 1, 20),
|
||||
arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35),
|
||||
arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36),
|
||||
arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36),
|
||||
arrayOf(" @m@localhost", 1, 13),
|
||||
arrayOf(" @m@localhost @a@localhost", 14, 26),
|
||||
arrayOf("@m@", 0, 3),
|
||||
arrayOf(" @m@ @a@asdf", 5, 12),
|
||||
arrayOf(" @m@ @a@", 5, 8),
|
||||
arrayOf(" @m@ @a@a", 5, 9),
|
||||
arrayOf(" @m@a @a@m", 6, 10),
|
||||
arrayOf("@m@m@", 5, 5),
|
||||
arrayOf("#tusky@husky", 12, 12),
|
||||
arrayOf(":tusky@husky", 12, 12),
|
||||
arrayOf("mention", 7, 7),
|
||||
arrayOf("ment10n", 7, 7),
|
||||
arrayOf("mentio_", 7, 7),
|
||||
arrayOf("#tusky", 0, 6),
|
||||
arrayOf("#@tusky", 7, 7),
|
||||
arrayOf("@#tusky", 7, 7),
|
||||
arrayOf(" @#tusky", 8, 8),
|
||||
arrayOf(":mastodon", 0, 9),
|
||||
arrayOf(":@mastodon", 10, 10),
|
||||
arrayOf("@:mastodon", 10, 10),
|
||||
arrayOf(" @:mastodon", 11, 11),
|
||||
arrayOf("#@:mastodon", 11, 11),
|
||||
arrayOf(" #@:mastodon", 12, 12)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val tokenizer = ComposeTokenizer()
|
||||
|
||||
@Test
|
||||
fun tokenIndices_matchExpectations() {
|
||||
Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
|
||||
Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue