Skip to content

Commit f37c6d1

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Add unit test for TextLayoutManager desired width optimization (#59202)
Summary: Add a Robolectric test that verifies `Layout.getDesiredWidth` is only called when necessary during text layout. The test confirms the optimization that avoids expensive width measurements for `EXACTLY` mode and for `AT_MOST` mode when boring text doesn't fit. It uses `mockStatic` to intercept calls to `Layout.getDesiredWidth` and verifies the correct number of invocations across different measure modes. Changelog: [Internal] Reviewed By: andrewdacenko Differential Revision: D125868474
1 parent e9443ba commit f37c6d1

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text
9+
10+
import android.annotation.SuppressLint
11+
import android.graphics.text.LineBreakConfig
12+
import android.text.BoringLayout
13+
import android.text.Layout
14+
import android.text.SpannableString
15+
import android.text.TextPaint
16+
import com.facebook.yoga.YogaMeasureMode
17+
import org.assertj.core.api.Assertions.assertThat
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import org.mockito.MockedStatic
21+
import org.mockito.Mockito.CALLS_REAL_METHODS
22+
import org.mockito.Mockito.mockStatic
23+
import org.mockito.Mockito.never
24+
import org.mockito.Mockito.times
25+
import org.mockito.kotlin.any
26+
import org.mockito.verification.VerificationMode
27+
import org.robolectric.RobolectricTestRunner
28+
import org.robolectric.annotation.Config
29+
30+
@RunWith(RobolectricTestRunner::class)
31+
@SuppressLint("NewApi")
32+
@Config(sdk = [35])
33+
class TextLayoutManagerDesiredWidthTest {
34+
35+
@Test
36+
fun `EXACTLY mode does not measure the desired width`() {
37+
mockStatic(Layout::class.java, CALLS_REAL_METHODS).use { layoutStatic ->
38+
val layout = createLayout(YogaMeasureMode.EXACTLY, boring = null)
39+
40+
layoutStatic.verifyDesiredWidthCalls(never())
41+
assertThat(layout.width).isEqualTo(LAYOUT_WIDTH.toInt())
42+
}
43+
}
44+
45+
@Test
46+
fun `AT_MOST mode does not measure the desired width when boring text does not fit`() {
47+
mockStatic(Layout::class.java, CALLS_REAL_METHODS).use { layoutStatic ->
48+
val layout = createLayout(YogaMeasureMode.AT_MOST, boring = boringMetrics(width = 500))
49+
50+
layoutStatic.verifyDesiredWidthCalls(never())
51+
assertThat(layout.width).isEqualTo(LAYOUT_WIDTH.toInt())
52+
}
53+
}
54+
55+
@Test
56+
fun `AT_MOST mode measures the desired width when the text is not boring`() {
57+
mockStatic(Layout::class.java, CALLS_REAL_METHODS).use { layoutStatic ->
58+
val layout = createLayout(YogaMeasureMode.AT_MOST, boring = null)
59+
60+
layoutStatic.verifyDesiredWidthCalls(times(1))
61+
assertThat(layout.width).isLessThanOrEqualTo(LAYOUT_WIDTH.toInt())
62+
}
63+
}
64+
65+
@Test
66+
fun `UNDEFINED mode measures the desired width when the text is not boring`() {
67+
mockStatic(Layout::class.java, CALLS_REAL_METHODS).use { layoutStatic ->
68+
val layout = createLayout(YogaMeasureMode.UNDEFINED, boring = null)
69+
70+
layoutStatic.verifyDesiredWidthCalls(times(1))
71+
assertThat(layout.lineCount).isEqualTo(1)
72+
}
73+
}
74+
75+
private fun MockedStatic<Layout>.verifyDesiredWidthCalls(mode: VerificationMode) {
76+
verify({ Layout.getDesiredWidth(any<CharSequence>(), any<TextPaint>()) }, mode)
77+
}
78+
79+
private fun boringMetrics(width: Int): BoringLayout.Metrics =
80+
BoringLayout.Metrics().apply {
81+
this.width = width
82+
ascent = -20
83+
descent = 5
84+
top = -22
85+
bottom = 6
86+
}
87+
88+
private fun createLayout(
89+
widthMode: YogaMeasureMode,
90+
boring: BoringLayout.Metrics?,
91+
lineBreakStyle: Int = LineBreakConfig.LINE_BREAK_STYLE_NONE,
92+
lineBreakWordStyle: Int = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE,
93+
): Layout =
94+
TextLayoutManager.createLayout(
95+
text = SpannableString("hello world"),
96+
boring = boring,
97+
width = LAYOUT_WIDTH,
98+
widthYogaMeasureMode = widthMode,
99+
includeFontPadding = false,
100+
textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY,
101+
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE,
102+
lineBreakStyle = lineBreakStyle,
103+
lineBreakWordStyle = lineBreakWordStyle,
104+
alignment = Layout.Alignment.ALIGN_NORMAL,
105+
justificationMode = 0,
106+
ellipsizeMode = null,
107+
maxNumberOfLines = 2,
108+
paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 26f },
109+
)
110+
111+
private companion object {
112+
const val LAYOUT_WIDTH = 200f
113+
}
114+
}

0 commit comments

Comments
 (0)