~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
98 lines · 3.2 KB Raw History
package social.grain

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.util.GrainLimits
import social.grain.util.TextLimit
import social.grain.util.graphemeCount
import social.grain.util.utf8ByteCount

/**
 * On the device rather than the JVM: grapheme segmentation comes from ICU, and
 * the JVM's own BreakIterator gives different — wrong — answers for flags and
 * ZWJ sequences.
 */
@RunWith(AndroidJUnit4::class)
class TextLimitsTest {

    @Test
    fun countsPlainTextOnePerCharacter() {
        assertEquals(5, "hello".graphemeCount())
        assertEquals(5, "hello".utf8ByteCount())
    }

    @Test
    fun countsAnAccentedCharacterAsOneGraphemeAndTwoBytes() {
        assertEquals(4, "café".graphemeCount())
        assertEquals(5, "café".utf8ByteCount())
    }

    /** The case String.length gets wrong: one glyph, two UTF-16 code units. */
    @Test
    fun countsAnEmojiAsOneGrapheme() {
        assertEquals(2, "📷".length)
        assertEquals(1, "📷".graphemeCount())
        assertEquals(4, "📷".utf8ByteCount())
    }

    /** A flag is two regional indicators; a reader sees one flag. */
    @Test
    fun countsAFlagAsOneGrapheme() {
        assertEquals(1, "🇬🇧".graphemeCount())
        assertEquals(8, "🇬🇧".utf8ByteCount())
    }

    @Test
    fun countsAZwjSequenceAsOneGrapheme() {
        assertEquals(1, "👩‍👩‍👧".graphemeCount())
    }

    @Test
    fun emptyTextCountsZero() {
        assertEquals(0, "".graphemeCount())
        assertEquals(0, "".utf8ByteCount())
    }

    @Test
    fun aGalleryTitleOfPlainTextIsBoundedByItsGraphemeCount() {
        assertTrue(GrainLimits.GalleryTitle.isWithin("a".repeat(100)))
        assertFalse(GrainLimits.GalleryTitle.isWithin("a".repeat(101)))
    }

    /**
     * The reason both units are checked. Twenty-six emoji is well inside the
     * gallery title's 100, but 104 bytes — over the cap the PDS enforces.
     */
    @Test
    fun aGalleryTitleWithinItsGraphemeCountCanStillBeOverInBytes() {
        val emoji = "📷".repeat(26)
        assertEquals(26, emoji.graphemeCount())
        assertEquals(104, emoji.utf8ByteCount())
        assertFalse(GrainLimits.GalleryTitle.isWithin(emoji))
    }

    /**
     * The profile lexicon gives itself ten bytes per grapheme, so for
     * displayName the grapheme count is what binds — 64 emoji is 256 bytes,
     * comfortably inside 640.
     */
    @Test
    fun aDisplayNameIsBoundedByGraphemesRatherThanBytes() {
        val emoji = "📷".repeat(64)
        assertEquals(256, emoji.utf8ByteCount())
        assertTrue(GrainLimits.DisplayName.isWithin(emoji))
        assertFalse(GrainLimits.DisplayName.isWithin("📷".repeat(65)))
    }

    @Test
    fun limitsMatchTheLexicons() {
        assertEquals(TextLimit(graphemes = 100, bytes = 100), GrainLimits.GalleryTitle)
        assertEquals(TextLimit(graphemes = 1000, bytes = 1000), GrainLimits.GalleryDescription)
        assertEquals(TextLimit(graphemes = 64, bytes = 640), GrainLimits.DisplayName)
        assertEquals(TextLimit(graphemes = 256, bytes = 2560), GrainLimits.Bio)
    }
}