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) } }