replace HtmlUtils with HtmlCompat (#1741)

* replace HtmlUtils with HtmlCompat

* fix tests
This commit is contained in:
Konrad Pozniak 2020-04-02 23:37:38 +02:00 committed by GitHub
parent f7434564df
commit 68f34152dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 65 additions and 165 deletions

View file

@ -20,6 +20,7 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.text.HtmlCompat;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -38,7 +39,6 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -884,7 +884,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getFavsText(Context context, int count) { protected CharSequence getFavsText(Context context, int count) {
if (count > 0) { if (count > 0) {
String countString = numberFormat.format(count); String countString = numberFormat.format(count);
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString)); return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else { } else {
return ""; return "";
} }
@ -893,7 +893,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getReblogsText(Context context, int count) { protected CharSequence getReblogsText(Context context, int count) {
if (count > 0) { if (count > 0) {
String countString = numberFormat.format(count); String countString = numberFormat.format(count);
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString)); return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else { } else {
return ""; return "";
} }

View file

@ -16,6 +16,8 @@
package com.keylesspalace.tusky.db package com.keylesspalace.tusky.db
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -27,7 +29,6 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.HtmlUtils
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
@ -128,7 +129,7 @@ class Converters {
if(spanned == null) { if(spanned == null) {
return null return null
} }
return HtmlUtils.toHtml(spanned) return spanned.toHtml()
} }
@TypeConverter @TypeConverter
@ -136,7 +137,7 @@ class Converters {
if(spannedString == null) { if(spannedString == null) {
return null return null
} }
return HtmlUtils.fromHtml(spannedString) return spannedString.parseAsHtml()
} }
@TypeConverter @TypeConverter

View file

@ -29,8 +29,6 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.network.TimelineCasesImpl import com.keylesspalace.tusky.network.TimelineCasesImpl
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.HtmlConverterImpl
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Singleton import javax.inject.Singleton
@ -84,9 +82,4 @@ class AppModule {
.build() .build()
} }
@Provides
@Singleton
fun providesHtmlConverter(): HtmlConverter {
return HtmlConverterImpl()
}
} }

View file

@ -6,17 +6,18 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRepository import com.keylesspalace.tusky.repository.TimelineRepository
import com.keylesspalace.tusky.repository.TimelineRepositoryImpl import com.keylesspalace.tusky.repository.TimelineRepositoryImpl
import com.keylesspalace.tusky.util.HtmlConverter
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@Module @Module
class RepositoryModule { class RepositoryModule {
@Provides @Provides
fun providesTimelineRepository(db: AppDatabase, mastodonApi: MastodonApi, fun providesTimelineRepository(
accountManager: AccountManager, gson: Gson, db: AppDatabase,
htmlConverter: HtmlConverter): TimelineRepository { mastodonApi: MastodonApi,
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson, accountManager: AccountManager,
htmlConverter) gson: Gson
): TimelineRepository {
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson)
} }
} }

View file

@ -15,24 +15,16 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import android.os.Parcel
import android.os.Parcelable
import android.text.Spanned import android.text.Spanned
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.keylesspalace.tusky.util.HtmlUtils import java.util.Date
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
import java.util.*
@Parcelize
data class Account( data class Account(
val id: String, val id: String,
@SerializedName("username") val localUsername: String, @SerializedName("username") val localUsername: String,
@SerializedName("acct") val username: String, @SerializedName("acct") val username: String,
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
val note: @WriteWith<SpannedParceler>() Spanned, val note: Spanned,
val url: String, val url: String,
val avatar: String, val avatar: String,
val header: String, val header: String,
@ -46,7 +38,7 @@ data class Account(
val fields: List<Field>? = emptyList(), //nullable for backward compatibility val fields: List<Field>? = emptyList(), //nullable for backward compatibility
val moved: Account? = null val moved: Account? = null
) : Parcelable { ) {
val name: String val name: String
get() = if (displayName.isNullOrEmpty()) { get() = if (displayName.isNullOrEmpty()) {
@ -86,31 +78,20 @@ data class Account(
fun isRemote(): Boolean = this.username != this.localUsername fun isRemote(): Boolean = this.username != this.localUsername
} }
@Parcelize
data class AccountSource( data class AccountSource(
val privacy: Status.Visibility, val privacy: Status.Visibility,
val sensitive: Boolean, val sensitive: Boolean,
val note: String, val note: String,
val fields: List<StringField>? val fields: List<StringField>?
): Parcelable )
@Parcelize
data class Field ( data class Field (
val name: String, val name: String,
val value: @WriteWith<SpannedParceler>() Spanned, val value: Spanned,
@SerializedName("verified_at") val verifiedAt: Date? @SerializedName("verified_at") val verifiedAt: Date?
): Parcelable )
@Parcelize
data class StringField ( data class StringField (
val name: String, val name: String,
val value: String val value: String
): Parcelable )
object SpannedParceler : Parceler<Spanned> {
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
override fun Spanned.write(parcel: Parcel, flags: Int) {
parcel.writeString(HtmlUtils.toHtml(this))
}
}

View file

@ -15,23 +15,19 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import android.os.Parcelable
import android.text.Spanned import android.text.Spanned
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
@Parcelize
data class Card( data class Card(
val url: String, val url: String,
val title: @WriteWith<SpannedParceler>() Spanned, val title: Spanned,
val description: @WriteWith<SpannedParceler>() Spanned, val description: Spanned,
@SerializedName("author_name") val authorName: String, @SerializedName("author_name") val authorName: String,
val image: String, val image: String,
val type: String, val type: String,
val width: Int, val width: Int,
val height: Int val height: Int
) : Parcelable { ) {
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode()

View file

@ -18,6 +18,8 @@ package com.keylesspalace.tusky.json;
import android.text.Spanned; import android.text.Spanned;
import android.text.SpannedString; import android.text.SpannedString;
import androidx.core.text.HtmlCompat;
import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -25,7 +27,6 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer; import com.google.gson.JsonSerializer;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -35,7 +36,9 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
throws JsonParseException { throws JsonParseException {
String string = json.getAsString(); String string = json.getAsString();
if (string != null) { if (string != null) {
return HtmlUtils.fromHtml(string); /* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
* all status contents do, so it should be trimmed. */
return (Spanned)trimTrailingWhitespace(HtmlCompat.fromHtml(string, HtmlCompat.FROM_HTML_MODE_LEGACY));
} else { } else {
return new SpannedString(""); return new SpannedString("");
} }
@ -43,6 +46,14 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
@Override @Override
public JsonElement serialize(Spanned src, Type typeOfSrc, JsonSerializationContext context) { public JsonElement serialize(Spanned src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(HtmlUtils.toHtml(src)); return new JsonPrimitive(HtmlCompat.toHtml(src, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL));
}
private static CharSequence trimTrailingWhitespace(CharSequence s) {
int i = s.length();
do {
i--;
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
return s.subSequence(0, i + 1);
} }
} }

View file

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.repository package com.keylesspalace.tusky.repository
import android.text.SpannedString import android.text.SpannedString
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.db.*
@ -9,7 +11,6 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK
import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.inc import com.keylesspalace.tusky.util.inc
import io.reactivex.Single import io.reactivex.Single
@ -40,8 +41,7 @@ class TimelineRepositoryImpl(
private val timelineDao: TimelineDao, private val timelineDao: TimelineDao,
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val gson: Gson, private val gson: Gson
private val htmlConverter: HtmlConverter
) : TimelineRepository { ) : TimelineRepository {
init { init {
@ -150,7 +150,7 @@ class TimelineRepositoryImpl(
for (status in statuses) { for (status in statuses) {
timelineDao.insertInTransaction( timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson), status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson), status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson) status.reblog?.account?.toEntity(accountId, gson)
) )
@ -214,7 +214,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""), content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
@ -269,7 +269,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""), content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
@ -362,7 +362,6 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
} }
fun Status.toEntity(timelineUserId: Long, fun Status.toEntity(timelineUserId: Long,
htmlConverter: HtmlConverter,
gson: Gson): TimelineStatusEntity { gson: Gson): TimelineStatusEntity {
val actionable = actionableStatus val actionable = actionableStatus
return TimelineStatusEntity( return TimelineStatusEntity(
@ -372,7 +371,7 @@ fun Status.toEntity(timelineUserId: Long,
authorServerId = actionable.account.id, authorServerId = actionable.account.id,
inReplyToId = actionable.inReplyToId, inReplyToId = actionable.inReplyToId,
inReplyToAccountId = actionable.inReplyToAccountId, inReplyToAccountId = actionable.inReplyToAccountId,
content = htmlConverter.toHtml(actionable.content), content = actionable.content.toHtml(),
createdAt = actionable.createdAt.time, createdAt = actionable.createdAt.time,
emojis = actionable.emojis.let(gson::toJson), emojis = actionable.emojis.let(gson::toJson),
reblogsCount = actionable.reblogsCount, reblogsCount = actionable.reblogsCount,
@ -385,7 +384,7 @@ fun Status.toEntity(timelineUserId: Long,
visibility = actionable.visibility, visibility = actionable.visibility,
attachments = actionable.attachments.let(gson::toJson), attachments = actionable.attachments.let(gson::toJson),
mentions = actionable.mentions.let(gson::toJson), mentions = actionable.mentions.let(gson::toJson),
application = actionable.let(gson::toJson), application = actionable.application.let(gson::toJson),
reblogServerId = reblog?.id, reblogServerId = reblog?.id,
reblogAccountId = reblog?.let { this.account.id }, reblogAccountId = reblog?.let { this.account.id },
poll = actionable.poll.let(gson::toJson), poll = actionable.poll.let(gson::toJson),

View file

@ -1,22 +0,0 @@
package com.keylesspalace.tusky.util
import android.text.Spanned
/**
* Abstracting away Android-specific things.
*/
interface HtmlConverter {
fun fromHtml(html: String): Spanned
fun toHtml(text: Spanned): String
}
internal class HtmlConverterImpl : HtmlConverter {
override fun fromHtml(html: String): Spanned {
return HtmlUtils.fromHtml(html)
}
override fun toHtml(text: Spanned): String {
return HtmlUtils.toHtml(text)
}
}

View file

@ -1,52 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* 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.util;
import android.os.Build;
import android.text.Html;
import android.text.Spanned;
public class HtmlUtils {
private static CharSequence trimTrailingWhitespace(CharSequence s) {
int i = s.length();
do {
i--;
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
return s.subSequence(0, i + 1);
}
public static Spanned fromHtml(String html) {
Spanned result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
} else {
result = Html.fromHtml(html);
}
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
* all status contents do, so it should be trimmed. */
return (Spanned) trimTrailingWhitespace(result);
}
public static String toHtml(Spanned text) {
String result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result = Html.toHtml(text, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
} else {
result = Html.toHtml(text);
}
return result;
}
}

View file

@ -171,15 +171,12 @@ class StatusViewHelper(private val itemView: View) {
sensitiveMediaWarning.visibility = View.GONE sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE sensitiveMediaShow.visibility = View.GONE
} else { } else {
sensitiveMediaWarning.text = if (sensitive) {
val hiddenContentText: String = if (sensitive) {
context.getString(R.string.status_sensitive_media_title) context.getString(R.string.status_sensitive_media_title)
} else { } else {
context.getString(R.string.status_media_hidden_title) context.getString(R.string.status_media_hidden_title)
} }
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
sensitiveMediaShow.setOnClickListener { v -> sensitiveMediaShow.setOnClickListener { v ->

View file

@ -18,10 +18,10 @@ package com.keylesspalace.tusky.viewdata
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.util.HtmlUtils
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -52,7 +52,7 @@ fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
} }
fun buildDescription(title: String, percent: Int, context: Context): Spanned { fun buildDescription(title: String, percent: Int, context: Context): Spanned {
return SpannableStringBuilder(HtmlUtils.fromHtml(context.getString(R.string.poll_percent_format, percent))) return SpannableStringBuilder(context.getString(R.string.poll_percent_format, percent).parseAsHtml())
.append(" ") .append(" ")
.append(title) .append(title)
} }

View file

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.fragment package com.keylesspalace.tusky.fragment
import android.text.Spanned import android.text.SpannableString
import android.text.SpannedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.gson.Gson import com.google.gson.Gson
import com.keylesspalace.tusky.SpanUtilsTest import com.keylesspalace.tusky.SpanUtilsTest
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
@ -12,7 +14,6 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.* import com.keylesspalace.tusky.repository.*
import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HtmlConverter
import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.isNull
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
@ -24,14 +25,18 @@ import io.reactivex.schedulers.TestScheduler
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class TimelineRepositoryTest { class TimelineRepositoryTest {
@Mock @Mock
lateinit var timelineDao: TimelineDao lateinit var timelineDao: TimelineDao
@ -56,15 +61,6 @@ class TimelineRepositoryTest {
domain = "domain.com", domain = "domain.com",
isActive = true isActive = true
) )
private val htmlConverter = object : HtmlConverter {
override fun fromHtml(html: String): Spanned {
return SpanUtilsTest.FakeSpannable(html)
}
override fun toHtml(text: Spanned): String {
return text.toString()
}
}
@Before @Before
fun setup() { fun setup() {
@ -74,8 +70,7 @@ class TimelineRepositoryTest {
gson = Gson() gson = Gson()
testScheduler = TestScheduler() testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler } RxJavaPlugins.setIoSchedulerHandler { testScheduler }
subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson, subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson)
htmlConverter)
} }
@Test @Test
@ -97,7 +92,7 @@ class TimelineRepositoryTest {
verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id)) verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id))
for (status in statuses) { for (status in statuses) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -129,7 +124,7 @@ class TimelineRepositoryTest {
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -159,7 +154,7 @@ class TimelineRepositoryTest {
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id) verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -201,7 +196,7 @@ class TimelineRepositoryTest {
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -246,7 +241,7 @@ class TimelineRepositoryTest {
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -263,7 +258,7 @@ class TimelineRepositoryTest {
val status = makeStatus("2") val status = makeStatus("2")
val dbStatus = makeStatus("1") val dbStatus = makeStatus("1")
val dbResult = TimelineStatusWithAccount() val dbResult = TimelineStatusWithAccount()
dbResult.status = dbStatus.toEntity(account.id, htmlConverter, gson) dbResult.status = dbStatus.toEntity(account.id, gson)
dbResult.account = status.account.toEntity(account.id, gson) dbResult.account = status.account.toEntity(account.id, gson)
whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
@ -297,7 +292,7 @@ class TimelineRepositoryTest {
return Status( return Status(
id = id, id = id,
account = account, account = account,
content = SpanUtilsTest.FakeSpannable("hello$id"), content = SpannableString("hello$id"),
createdAt = Date(), createdAt = Date(),
emojis = listOf(), emojis = listOf(),
reblogsCount = 3, reblogsCount = 3,
@ -328,7 +323,7 @@ class TimelineRepositoryTest {
localUsername = "test$id", localUsername = "test$id",
username = "test$id@example.com", username = "test$id@example.com",
displayName = "Example Account $id", displayName = "Example Account $id",
note = SpanUtilsTest.FakeSpannable("Note! $id"), note = SpannableString("Note! $id"),
url = "https://example.com/@test$id", url = "https://example.com/@test$id",
avatar = "avatar$id", avatar = "avatar$id",
header = "Header$id", header = "Header$id",