Merge pull request #2040 from element-hq/feature/bma/fixLinkify
fix linkify
This commit is contained in:
commit
d6ada81bcd
9 changed files with 127 additions and 53 deletions
|
|
@ -60,8 +60,9 @@ class LoggedInPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.showSyncSpinner).isFalse()
|
assertThat(initialState.showSyncSpinner).isFalse()
|
||||||
|
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
|
||||||
consumeItemsUntilPredicate { it.showSyncSpinner }
|
consumeItemsUntilPredicate { it.showSyncSpinner }
|
||||||
roomListService.postState(RoomListService.State.Running)
|
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Hide)
|
||||||
consumeItemsUntilPredicate { !it.showSyncSpinner }
|
consumeItemsUntilPredicate { !it.showSyncSpinner }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import android.text.style.URLSpan
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.text.util.LinkifyCompat
|
import androidx.core.text.util.LinkifyCompat
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||||
|
|
@ -68,12 +69,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
|
|
||||||
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
|
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
|
||||||
return when (val messageType = content.type) {
|
return when (val messageType = content.type) {
|
||||||
is EmoteMessageType -> TimelineItemEmoteContent(
|
is EmoteMessageType -> {
|
||||||
body = "* $senderDisplayName ${messageType.body}",
|
val emoteBody = "* $senderDisplayName ${messageType.body}"
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
|
TimelineItemEmoteContent(
|
||||||
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName"),
|
body = emoteBody,
|
||||||
isEdited = content.isEdited,
|
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
|
||||||
)
|
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
|
||||||
|
isEdited = content.isEdited,
|
||||||
|
)
|
||||||
|
}
|
||||||
is ImageMessageType -> {
|
is ImageMessageType -> {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemImageContent(
|
TimelineItemImageContent(
|
||||||
|
|
@ -171,21 +175,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
is NoticeMessageType -> TimelineItemNoticeContent(
|
is NoticeMessageType -> TimelineItemNoticeContent(
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
||||||
formattedBody = parseHtml(messageType.formatted),
|
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
is TextMessageType -> {
|
is TextMessageType -> {
|
||||||
TimelineItemTextContent(
|
TimelineItemTextContent(
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
htmlDocument = messageType.formatted?.toHtmlDocument(),
|
||||||
formattedBody = parseHtml(messageType.formatted),
|
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is OtherMessageType -> TimelineItemTextContent(
|
is OtherMessageType -> TimelineItemTextContent(
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
htmlDocument = null,
|
htmlDocument = null,
|
||||||
formattedBody = null,
|
formattedBody = messageType.body.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +230,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
Pair(start, end)
|
Pair(start, end)
|
||||||
}
|
}
|
||||||
// Find and set as URLSpans any links present in the text
|
// Find and set as URLSpans any links present in the text
|
||||||
LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS)
|
LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||||
// Restore old spans if they don't conflict with the new ones
|
// Restore old spans if they don't conflict with the new ones
|
||||||
for ((urlSpan, location) in oldURLSpans) {
|
for ((urlSpan, location) in oldURLSpans) {
|
||||||
val (start, end) = location
|
val (start, end) = location
|
||||||
|
|
@ -237,3 +241,11 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("USELESS_ELVIS")
|
||||||
|
private fun String.withLinks(): CharSequence? {
|
||||||
|
/* Note: toSpannable() can return null when running unit tests */
|
||||||
|
val spannable = toSpannable() ?: return null
|
||||||
|
val addedLinks = LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||||
|
return spannable.takeIf { addedLinks }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@
|
||||||
package io.element.android.features.messages.impl.timeline.factories.event
|
package io.element.android.features.messages.impl.timeline.factories.event
|
||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.Spanned
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.inSpans
|
import androidx.core.text.inSpans
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
|
@ -144,9 +145,40 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
assertThat(result).isEqualTo(expected)
|
assertThat(result).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test create TextMessageType with simple link`() = runTest {
|
||||||
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
|
val result = sut.create(
|
||||||
|
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
|
||||||
|
senderDisplayName = "Bob",
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
) as TimelineItemTextContent
|
||||||
|
val expected = TimelineItemTextContent(
|
||||||
|
body = "https://www.example.org",
|
||||||
|
htmlDocument = null,
|
||||||
|
plainText = "https://www.example.org",
|
||||||
|
isEdited = false,
|
||||||
|
formattedBody = buildSpannedString {
|
||||||
|
inSpans(URLSpan("https://www.example.org")) {
|
||||||
|
append("https://www.example.org")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(result.body).isEqualTo(expected.body)
|
||||||
|
assertThat(result.htmlDocument).isEqualTo(expected.htmlDocument)
|
||||||
|
assertThat(result.plainText).isEqualTo(expected.plainText)
|
||||||
|
assertThat(result.isEdited).isEqualTo(expected.isEdited)
|
||||||
|
assertThat(result.formattedBody).isInstanceOf(Spanned::class.java)
|
||||||
|
val spanned = result.formattedBody as Spanned
|
||||||
|
assertThat(spanned.toString()).isEqualTo("https://www.example.org")
|
||||||
|
val urlSpans = spanned.getSpans(0, spanned.length, URLSpan::class.java)
|
||||||
|
assertThat(urlSpans).hasLength(1)
|
||||||
|
assertThat(urlSpans[0].url).isEqualTo("https://www.example.org")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test create TextMessageType with HTML formatted body`() = runTest {
|
fun `test create TextMessageType with HTML formatted body`() = runTest {
|
||||||
val expected = SpannableStringBuilder().apply {
|
val expected = buildSpannedString {
|
||||||
append("link to ")
|
append("link to ")
|
||||||
inSpans(URLSpan("https://matrix.org")) {
|
inSpans(URLSpan("https://matrix.org")) {
|
||||||
append("https://matrix.org")
|
append("https://matrix.org")
|
||||||
|
|
@ -160,10 +192,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
htmlConverterTransform = { expected }
|
htmlConverterTransform = { expected }
|
||||||
)
|
)
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = TextMessageType(
|
content = createMessageContent(
|
||||||
body = "body",
|
type = TextMessageType(
|
||||||
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
|
body = "body",
|
||||||
)),
|
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
|
||||||
|
)
|
||||||
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
|
|
@ -176,10 +210,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
htmlConverterTransform = { it }
|
htmlConverterTransform = { it }
|
||||||
)
|
)
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = TextMessageType(
|
content = createMessageContent(
|
||||||
body = "body",
|
type = TextMessageType(
|
||||||
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
|
body = "body",
|
||||||
)),
|
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
|
||||||
|
)
|
||||||
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
|
|
@ -520,10 +556,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
fun `test create NoticeMessageType with HTML formatted body`() = runTest {
|
fun `test create NoticeMessageType with HTML formatted body`() = runTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = NoticeMessageType(
|
content = createMessageContent(
|
||||||
body = "body",
|
type = NoticeMessageType(
|
||||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
body = "body",
|
||||||
)),
|
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||||
|
)
|
||||||
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
|
|
@ -552,10 +590,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
fun `test create EmoteMessageType with HTML formatted body`() = runTest {
|
fun `test create EmoteMessageType with HTML formatted body`() = runTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = EmoteMessageType(
|
content = createMessageContent(
|
||||||
body = "body",
|
type = EmoteMessageType(
|
||||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
body = "body",
|
||||||
)),
|
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||||
|
)
|
||||||
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,15 @@ class PollHistoryPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val room = FakeMatrixRoom(
|
private val timeline = aPollTimeline(
|
||||||
matrixTimeline = aPollTimeline(
|
polls = mapOf(
|
||||||
polls = mapOf(
|
AN_EVENT_ID to anOngoingPollContent(),
|
||||||
AN_EVENT_ID to anOngoingPollContent(),
|
AN_EVENT_ID_2 to anEndedPollContent()
|
||||||
AN_EVENT_ID_2 to anEndedPollContent()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
private val room = FakeMatrixRoom(
|
||||||
|
matrixTimeline = timeline
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial states`() = runTest {
|
fun `present - initial states`() = runTest {
|
||||||
|
|
@ -134,10 +135,14 @@ class PollHistoryPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
consumeItemsUntilPredicate {
|
consumeItemsUntilPredicate {
|
||||||
it.pollHistoryItems.size == 2 && !it.isLoading
|
it.pollHistoryItems.size == 2
|
||||||
}.last().also { state ->
|
|
||||||
state.eventSink(PollHistoryEvents.LoadMore)
|
|
||||||
}
|
}
|
||||||
|
timeline.updatePaginationState {
|
||||||
|
copy(isBackPaginating = false)
|
||||||
|
}
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
assertThat(loadedState.isLoading).isFalse()
|
||||||
|
loadedState.eventSink(PollHistoryEvents.LoadMore)
|
||||||
consumeItemsUntilPredicate {
|
consumeItemsUntilPredicate {
|
||||||
it.isLoading
|
it.isLoading
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
|
@ -292,11 +293,10 @@ class EditUserProfilePresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name "))
|
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name "))
|
||||||
initialState.eventSink(EditUserProfileEvents.Save)
|
initialState.eventSink(EditUserProfileEvents.Save)
|
||||||
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && !matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
|
consumeItemsUntilTimeout()
|
||||||
assertThat(matrixClient.setDisplayNameCalled).isFalse()
|
assertThat(matrixClient.setDisplayNameCalled).isFalse()
|
||||||
assertThat(matrixClient.uploadAvatarCalled).isFalse()
|
assertThat(matrixClient.uploadAvatarCalled).isFalse()
|
||||||
assertThat(matrixClient.removeAvatarCalled).isFalse()
|
assertThat(matrixClient.removeAvatarCalled).isFalse()
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,17 +192,22 @@ class RoomListPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
|
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
|
||||||
val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
|
skipItems(3)
|
||||||
|
val loadedState = awaitItem()
|
||||||
// Test filtering with result
|
// Test filtering with result
|
||||||
|
assertThat(loadedState.roomList.size).isEqualTo(1)
|
||||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
||||||
val withFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 1 }.last()
|
skipItems(1)
|
||||||
|
val withFilteredRoomState = awaitItem()
|
||||||
|
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||||
assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||||
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||||
assertThat(withFilteredRoomState.filteredRoomList.first())
|
assertThat(withFilteredRoomState.filteredRoomList.first())
|
||||||
.isEqualTo(aRoomListRoomSummary)
|
.isEqualTo(aRoomListRoomSummary)
|
||||||
// Test filtering without result
|
// Test filtering without result
|
||||||
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||||
val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last()
|
skipItems(1)
|
||||||
|
val withNotFilteredRoomState = awaitItem()
|
||||||
assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
|
assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
|
||||||
assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
|
assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ fun ClickableLinkText(
|
||||||
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
||||||
val original = this
|
val original = this
|
||||||
val spannable = SpannableString(this.text)
|
val spannable = SpannableString(this.text)
|
||||||
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS)
|
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||||
|
|
||||||
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
|
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
|
||||||
return buildAnnotatedString {
|
return buildAnnotatedString {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ class FakeRoomListService : RoomListService {
|
||||||
roomListStateFlow.emit(state)
|
roomListStateFlow.emit(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun postSyncIndicator(value: RoomListService.SyncIndicator) {
|
||||||
|
syncIndicatorStateFlow.emit(value)
|
||||||
|
}
|
||||||
|
|
||||||
var latestSlidingSyncRange: IntRange? = null
|
var latestSlidingSyncRange: IntRange? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.tests.testutils
|
||||||
import app.cash.turbine.Event
|
import app.cash.turbine.Event
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
import app.cash.turbine.withTurbineTimeout
|
import app.cash.turbine.withTurbineTimeout
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
* @return the list of consumed items.
|
* @return the list of consumed items.
|
||||||
*/
|
*/
|
||||||
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List<T> {
|
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List<T> {
|
||||||
return consumeItemsUntilPredicate(timeout) { false }
|
return consumeItemsUntilPredicate(timeout, ignoreTimeoutError = true) { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,22 +49,29 @@ suspend fun <T : Any> ReceiveTurbine<T>.awaitLastSequentialItem(): T {
|
||||||
*/
|
*/
|
||||||
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilPredicate(
|
suspend fun <T : Any> ReceiveTurbine<T>.consumeItemsUntilPredicate(
|
||||||
timeout: Duration = 100.milliseconds,
|
timeout: Duration = 100.milliseconds,
|
||||||
|
ignoreTimeoutError: Boolean = false,
|
||||||
predicate: (T) -> Boolean,
|
predicate: (T) -> Boolean,
|
||||||
): List<T> {
|
): List<T> {
|
||||||
val items = ArrayList<T>()
|
val items = ArrayList<T>()
|
||||||
tryOrNull {
|
var exitLoop = false
|
||||||
var foundItemOrFinished = false
|
try {
|
||||||
while (!foundItemOrFinished) {
|
while (!exitLoop) {
|
||||||
when (val event = withTurbineTimeout(timeout) { awaitEvent() }) {
|
when (val event = withTurbineTimeout(timeout) { awaitEvent() }) {
|
||||||
is Event.Item<T> -> {
|
is Event.Item<T> -> {
|
||||||
items.add(event.value)
|
items.add(event.value)
|
||||||
if (predicate(event.value)) {
|
exitLoop = predicate(event.value)
|
||||||
foundItemOrFinished = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event.Complete, is Event.Error -> foundItemOrFinished = true
|
Event.Complete -> error("Unexpected complete")
|
||||||
|
is Event.Error -> throw event.throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (assertionError: AssertionError) {
|
||||||
|
// TurbineAssertionError is internal :/, so rely on the message
|
||||||
|
if (assertionError.message?.startsWith("No value produced in").orFalse() && ignoreTimeoutError) {
|
||||||
|
// Timeout, ignore
|
||||||
|
} else {
|
||||||
|
throw assertionError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue