@@ -77,15 +77,13 @@ class ReactTextViewAccessibilityDelegateTest {
7777 }
7878
7979 @Test
80- fun reactTextViewAccessibilityNodeText_preservesMixedClickableAndUrlSpans () {
80+ fun reactTextViewAccessibilityNodeText_preservesMixedClickableAndVisualSpans () {
8181 val clickableSpan =
8282 object : ClickableSpan () {
8383 override fun onClick (widget : View ) = Unit
8484 }
85- val urlSpan = URLSpan (" https://reactnative.dev" )
8685 val text = createStyledText(" Read docs now" )
8786 text.setSpan(clickableSpan, 5 , 9 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
88- text.setSpan(urlSpan, 0 , 4 , Spanned .SPAN_INCLUSIVE_EXCLUSIVE )
8987 val textView = createReactTextView(text)
9088
9189 val nodeInfo = createNodeInfo(textView)
@@ -96,11 +94,25 @@ class ReactTextViewAccessibilityDelegateTest {
9694 assertThat(nodeInfo.text.toString()).isEqualTo(" Read docs now" )
9795 assertAccessibilityTextDoesNotHaveVisualSpans(accessibilityText)
9896 assertPreservedSpanMatchesSource(sourceText, accessibilityText, clickableSpan)
99- assertPreservedSpanMatchesSource(sourceText, accessibilityText, urlSpan)
10097 }
10198
10299 @Test
103- fun preparedLayoutTextViewAccessibilityNodeText_stripsStyleSpansAndPreservesClickableSpan () {
100+ fun reactTextViewAccessibilityNodeText_preservesUrlSpanSemantics () {
101+ val urlSpan = URLSpan (" https://reactnative.dev" )
102+ val text = createStyledText(" React Native" )
103+ text.setSpan(urlSpan, 0 , 5 , Spanned .SPAN_INCLUSIVE_EXCLUSIVE )
104+ val textView = createReactTextView(text)
105+
106+ val nodeInfo = createNodeInfo(textView)
107+ val accessibilityText = nodeInfo.text as Spanned
108+
109+ assertThat(nodeInfo.text.toString()).isEqualTo(" React Native" )
110+ assertAccessibilityTextDoesNotHaveVisualSpans(accessibilityText)
111+ assertAccessibilityTextHasClickableSpan(accessibilityText, 0 , 5 , Spanned .SPAN_INCLUSIVE_EXCLUSIVE )
112+ }
113+
114+ @Test
115+ fun preparedLayoutTextViewAccessibilityNodeText_keepsOnlyClickableSpans () {
104116 val text = createStyledText(" Prepared text" )
105117 val clickableSpan =
106118 object : ClickableSpan () {
@@ -134,6 +146,37 @@ class ReactTextViewAccessibilityDelegateTest {
134146 assertPreservedSpanMatchesSource(textView.text as Spanned , accessibilityText, clickableSpan)
135147 }
136148
149+ @Test
150+ fun reactTextViewAccessibilityNodeText_trimsLongTextWithoutSplittingSurrogatePairs () {
151+ val crossingBoundarySpan =
152+ object : ClickableSpan () {
153+ override fun onClick (widget : View ) = Unit
154+ }
155+ val outsideRetainedTextSpan =
156+ object : ClickableSpan () {
157+ override fun onClick (widget : View ) = Unit
158+ }
159+ val text = SpannableString (" ${" a" .repeat(99_999 )} \uD83D\uDE00 b" )
160+ text.setSpan(crossingBoundarySpan, 99_998 , 100_001 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
161+ text.setSpan(outsideRetainedTextSpan, 100_001 , 100_002 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
162+ val textView = createReactTextView(text)
163+
164+ val nodeInfo = createNodeInfo(textView)
165+ val accessibilityText = nodeInfo.text as Spanned
166+
167+ assertThat(nodeInfo.text.length).isEqualTo(99_999 )
168+ assertThat(nodeInfo.text.toString()).doesNotEndWith(" \uD83D " )
169+ assertPreservedSpanMatchesRange(
170+ accessibilityText,
171+ crossingBoundarySpan,
172+ 99_998 ,
173+ 99_999 ,
174+ Spanned .SPAN_EXCLUSIVE_EXCLUSIVE ,
175+ )
176+ assertThat(accessibilityText.getSpans(0 , accessibilityText.length, ClickableSpan ::class .java))
177+ .doesNotContain(outsideRetainedTextSpan)
178+ }
179+
137180 private fun createReactTextViewWithStyledText (text : String ): ReactTextView {
138181 return createReactTextView(createStyledText(text))
139182 }
@@ -194,18 +237,45 @@ class ReactTextViewAccessibilityDelegateTest {
194237 sourceText : Spanned ,
195238 accessibilityText : Spanned ,
196239 sourceSpan : Any ,
240+ ) {
241+ assertPreservedSpanMatchesRange(
242+ accessibilityText,
243+ sourceSpan,
244+ sourceText.getSpanStart(sourceSpan),
245+ sourceText.getSpanEnd(sourceSpan),
246+ sourceText.getSpanFlags(sourceSpan),
247+ )
248+ }
249+
250+ private fun assertPreservedSpanMatchesRange (
251+ accessibilityText : Spanned ,
252+ sourceSpan : Any ,
253+ start : Int ,
254+ end : Int ,
255+ flags : Int ,
197256 ) {
198257 val preservedSpans =
199258 accessibilityText
200- .getSpans(
201- sourceText.getSpanStart(sourceSpan),
202- sourceText.getSpanEnd(sourceSpan),
203- sourceSpan.javaClass,
204- )
205- .filter { accessibilityText.getSpanStart(it) == sourceText.getSpanStart(sourceSpan) }
206- .filter { accessibilityText.getSpanEnd(it) == sourceText.getSpanEnd(sourceSpan) }
207- .filter { accessibilityText.getSpanFlags(it) == sourceText.getSpanFlags(sourceSpan) }
208-
259+ .getSpans(start, end, sourceSpan.javaClass)
260+ .filter { accessibilityText.getSpanStart(it) == start }
261+ .filter { accessibilityText.getSpanEnd(it) == end }
262+ .filter { accessibilityText.getSpanFlags(it) == flags }
209263 assertThat(preservedSpans).isNotEmpty()
210264 }
265+
266+ private fun assertAccessibilityTextHasClickableSpan (
267+ accessibilityText : Spanned ,
268+ start : Int ,
269+ end : Int ,
270+ flags : Int ,
271+ ) {
272+ val clickableSpans =
273+ accessibilityText
274+ .getSpans(start, end, ClickableSpan ::class .java)
275+ .filter { accessibilityText.getSpanStart(it) == start }
276+ .filter { accessibilityText.getSpanEnd(it) == end }
277+ .filter { accessibilityText.getSpanFlags(it) == flags }
278+
279+ assertThat(clickableSpans).isNotEmpty()
280+ }
211281}
0 commit comments