fix(compose): keep the bullet button tappable by appending the font-size control last

The font-size dropdown was inserted before the block-marker buttons, and its
wide "Default"/"N pt" anchor pushed the "•" bullet button past the right edge
of the horizontally-scrolling toolbar on the Pixel 2 E2E device (411dp wide,
minus the compose column's 16dp padding = 379dp usable). ComposeScreenTest's
formattingToolbar_bulletButtonMarksTheLineAndSendsItAsHtml taps the bullet
without scrolling first, so performClick targeted a center that was clipped
off-screen and the tap silently missed — the line was never marked, failing
all 8 instrumented legs deterministically (expected "• Buy milk", got "Buy milk").

The block-toggle logic was never touched; this was pure toolbar overflow. Move
FontSizePicker to the end of the toolbar (after the link button) so every
pre-existing glyph button keeps the exact position it has on main and the
bullet stays within the initial viewport. Add a comment recording the ordering
constraint for future toolbar tickets.

Also add a JVM unit test (RichTextEditorTest) that drives the same bullet-tap
flow through applyBlock + RichTextHtml.toHtml, pinning "• Buy milk" and
<ul><li>Buy milk</li></ul> so a regression in that block/HTML path is caught
by testDebugUnitTest without an emulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 23:13:59 -05:00
co-authored by Claude Opus 4.8
parent 1634c57837
commit 9c6a969c17
2 changed files with 22 additions and 2 deletions
@@ -269,8 +269,6 @@ private fun FormattingToolbar(
strikethrough = true,
onClick = { onToggleStyle(RichStyle.Strikethrough) },
)
val fontSizePt = RichTextEditing.styleAt(content, start, end, RichStyle.FontSize::class.java)?.pt
FontSizePicker(selectedPt = fontSizePt, onSelect = onFontSize)
val fontColorArgb = RichTextEditing.styleAt(content, start, end, RichStyle.FontColor::class.java)?.argb
FormatButton(
label = "A",
@@ -311,6 +309,14 @@ private fun FormattingToolbar(
active = false,
onClick = onLink,
)
// The font-size dropdown trails every glyph button on purpose. The toolbar overflows the
// screen width and scrolls horizontally, and the compose E2E taps the "•" bullet button
// *without* scrolling first (see ComposeScreenTest.formattingToolbar_bulletButtonMarksTheLine...),
// so its click lands on the button's on-screen center. Any control inserted *before* the block
// buttons shifts them right and can push the bullet past the viewport, making that tap miss —
// so this wider control is appended last, leaving every pre-existing button in its tested spot.
val fontSizePt = RichTextEditing.styleAt(content, start, end, RichStyle.FontSize::class.java)?.pt
FontSizePicker(selectedPt = fontSizePt, onSelect = onFontSize)
}
}
@@ -19,6 +19,7 @@ import org.libremail.richtext.RichSpan
import org.libremail.richtext.RichStyle
import org.libremail.richtext.RichTextContent
import org.libremail.richtext.RichTextEditing
import org.libremail.richtext.RichTextHtml
import org.libremail.richtext.imageToken
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -240,6 +241,19 @@ class RichTextEditorTest {
assertEquals("1. a\n2. b", result.annotatedString.text)
}
@Test
fun `applyBlock bullet on an end-of-text caret marks the line and serializes to ul li html`() {
// The JVM-layer twin of ComposeScreenTest.formattingToolbar_bulletButtonMarksTheLineAndSendsItAsHtml:
// a bullet tap on the end-of-text caret that typing leaves must mark the whole line and serialize
// to a real list. Pinning it here catches a regression in the block-toggle/HTML flow without an
// emulator; the instrumented test additionally guards that the toolbar button stays tappable.
val value = field("Buy milk", TextRange(8))
val bulleted = applyBlock(value, BlockMarker.BULLET, linkColor, noFont)
val content = bulleted.annotatedString.toRichContent()
assertEquals("• Buy milk", content.text)
assertEquals("<ul><li>Buy milk</li></ul>", RichTextHtml.toHtml(content))
}
// --- applyLink ---
@Test