Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
5.6 kB · 141 lines
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142package social.grain
import androidx.compose.ui.graphics.Colorimport androidx.test.ext.junit.runners.AndroidJUnit4import kotlinx.serialization.json.Jsonimport kotlinx.serialization.json.jsonObjectimport org.junit.Assert.assertEqualsimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.data.model.Facetimport social.grain.ui.components.buildRichText
/** * Facet indices are UTF-8 **byte** offsets, not character offsets. * * Slicing the string by character index looks correct in English and silently * mislabels every link the moment a comment contains an emoji or an accent — * the highlight drifts left and the tap target lands on the wrong word. These * pin the byte-accurate path. */@RunWith(AndroidJUnit4::class)class RichTextFacetTest {
private val json = Json { ignoreUnknownKeys = true }
private fun facet(start: Int, end: Int, type: String, key: String, value: String): Facet = Facet( index = Facet.ByteSlice(start, end), features = listOf( json.parseToJsonElement("""{"${'$'}type":"$type","$key":"$value"}""").jsonObject, ), )
private fun render(text: String, facets: List<Facet>?) = buildRichText(text, facets, Color.Black, Color.Blue, null, null)
private fun mention(start: Int, end: Int, did: String = "did:plc:abc") = facet(start, end, "app.bsky.richtext.facet#mention", "did", did)
@Test fun facetSpanIsPlacedByByteOffsetNotCharacterOffset() { // "📷" is four UTF-8 bytes but two Kotlin chars, so byte and char // offsets diverge by two from here on. val text = "📷 shot by @alice.test today" val handle = "@alice.test" val byteStart = text.substringBefore(handle).toByteArray(Charsets.UTF_8).size val byteEnd = byteStart + handle.toByteArray(Charsets.UTF_8).size
val annotated = render(text, listOf(mention(byteStart, byteEnd)))
assertEquals("Rendered text must match the input exactly", text, annotated.text)
val links = annotated.getLinkAnnotations(0, annotated.length) assertEquals(1, links.size) assertEquals( "The link must cover exactly the handle", handle, annotated.text.substring(links[0].start, links[0].end), ) }
@Test fun accentedTextKeepsLinkAlignment() { val text = "café visit with @bob.test" val handle = "@bob.test" val byteStart = text.substringBefore(handle).toByteArray(Charsets.UTF_8).size val byteEnd = byteStart + handle.toByteArray(Charsets.UTF_8).size
val annotated = render(text, listOf(mention(byteStart, byteEnd))) val link = annotated.getLinkAnnotations(0, annotated.length).single() assertEquals(handle, annotated.text.substring(link.start, link.end)) }
@Test fun multipleFacetsAllLandOnTheirOwnSpans() { val text = "@alice.test and @bob.test shot #35mm" fun spanOf(needle: String): Pair<Int, Int> { val start = text.substringBefore(needle).toByteArray(Charsets.UTF_8).size return start to start + needle.toByteArray(Charsets.UTF_8).size }
val (aStart, aEnd) = spanOf("@alice.test") val (bStart, bEnd) = spanOf("@bob.test") val (tStart, tEnd) = spanOf("#35mm")
val annotated = render( text, listOf( mention(aStart, aEnd, "did:plc:alice"), mention(bStart, bEnd, "did:plc:bob"), facet(tStart, tEnd, "app.bsky.richtext.facet#tag", "tag", "35mm"), ), )
val spans = annotated.getLinkAnnotations(0, annotated.length) .map { annotated.text.substring(it.start, it.end) } assertEquals(listOf("@alice.test", "@bob.test", "#35mm"), spans) assertEquals(text, annotated.text) }
@Test fun anOutOfRangeFacetIsIgnoredRatherThanCrashing() { // The appview can hand back a facet whose end runs past the text if the // record was written with a different encoding assumption. val text = "short" val annotated = render(text, listOf(mention(0, 900))) assertEquals(text, annotated.text) }
@Test fun textWithNoFacetsFallsBackToDetectingMentionsAndTags() { val annotated = render("hi @alice.test see #35mm", null) val spans = annotated.getLinkAnnotations(0, annotated.length) .map { annotated.text.substring(it.start, it.end) } assertEquals(listOf("@alice.test", "#35mm"), spans) }
@Test fun aTagStartingWithADigitIsStillLinkified() { // "#35mm" and "#120film" are the tags this app exists for. val annotated = render("shot on #35mm and #120film", null) val spans = annotated.getLinkAnnotations(0, annotated.length) .map { annotated.text.substring(it.start, it.end) } assertEquals(listOf("#35mm", "#120film"), spans) }
@Test fun aPurelyNumericHashIsNotATag() { // "ranked #1" is prose, not a tag. val annotated = render("ranked #1 in #2024", null) assertEquals(0, annotated.getLinkAnnotations(0, annotated.length).size) }
@Test fun aUrlIsNotCarvedUpByAHashInIt() { val annotated = render("see https://grain.social/x#tag now", null) val spans = annotated.getLinkAnnotations(0, annotated.length) .map { annotated.text.substring(it.start, it.end) } assertEquals(listOf("https://grain.social/x#tag"), spans) }}