@@ -16,8 +16,11 @@ import android.text.Spanned
1616import android.text.StaticLayout
1717import android.text.TextPaint
1818import android.text.style.AbsoluteSizeSpan
19+ import android.text.style.BackgroundColorSpan
1920import android.text.style.ClickableSpan
2021import android.text.style.ForegroundColorSpan
22+ import android.text.style.StyleSpan
23+ import android.text.style.URLSpan
2124import android.view.View
2225import androidx.core.view.ViewCompat
2326import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -37,7 +40,7 @@ class ReactTextViewAccessibilityDelegateTest {
3740
3841 assertSourceTextKeepsStyleSpans(textView.text)
3942 assertThat(nodeInfo.text.toString()).isEqualTo(" Start" )
40- assertThat (nodeInfo.text).isNotInstanceOf( Spanned :: class .java )
43+ assertAccessibilityTextDoesNotHaveVisualSpans (nodeInfo.text)
4144 }
4245
4346 @Test
@@ -49,31 +52,61 @@ class ReactTextViewAccessibilityDelegateTest {
4952
5053 assertThat(textView.contentDescription.toString()).isEqualTo(" Custom label" )
5154 assertThat(nodeInfo.text.toString()).isEqualTo(" Visible text" )
52- assertThat (nodeInfo.text).isNotInstanceOf( Spanned :: class .java )
55+ assertAccessibilityTextDoesNotHaveVisualSpans (nodeInfo.text)
5356 }
5457
5558 @Test
56- fun reactTextViewAccessibilityNodeText_doesNotStripSourceClickableSpans () {
59+ fun reactTextViewAccessibilityNodeText_preservesWholeTextClickableSpan () {
5760 val clickableSpan =
5861 object : ClickableSpan () {
5962 override fun onClick (widget : View ) = Unit
6063 }
6164 val text = createStyledText(" Read docs" )
62- text.setSpan(clickableSpan, 5 , 9 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
65+ text.setSpan(clickableSpan, 0 , text.length , Spanned .SPAN_INCLUSIVE_EXCLUSIVE )
6366 val textView = createReactTextView(text)
6467
6568 val nodeInfo = createNodeInfo(textView)
6669 val sourceText = textView.text as Spanned
70+ val accessibilityText = nodeInfo.text as Spanned
6771
68- assertThat(sourceText.getSpans(0 , sourceText.length, ClickableSpan ::class .java)).isNotEmpty()
6972 assertSourceTextKeepsStyleSpans(sourceText)
73+ assertThat(ReactTextViewAccessibilityDelegate .AccessibilityLinks (sourceText).size()).isEqualTo(0 )
7074 assertThat(nodeInfo.text.toString()).isEqualTo(" Read docs" )
71- assertThat(nodeInfo.text).isNotInstanceOf(Spanned ::class .java)
75+ assertAccessibilityTextDoesNotHaveVisualSpans(accessibilityText)
76+ assertPreservedSpanMatchesSource(sourceText, accessibilityText, clickableSpan)
77+ }
78+
79+ @Test
80+ fun reactTextViewAccessibilityNodeText_preservesMixedClickableAndUrlSpans () {
81+ val clickableSpan =
82+ object : ClickableSpan () {
83+ override fun onClick (widget : View ) = Unit
84+ }
85+ val urlSpan = URLSpan (" https://reactnative.dev" )
86+ val text = createStyledText(" Read docs now" )
87+ text.setSpan(clickableSpan, 5 , 9 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
88+ text.setSpan(urlSpan, 0 , 4 , Spanned .SPAN_INCLUSIVE_EXCLUSIVE )
89+ val textView = createReactTextView(text)
90+
91+ val nodeInfo = createNodeInfo(textView)
92+ val sourceText = textView.text as Spanned
93+ val accessibilityText = nodeInfo.text as Spanned
94+
95+ assertSourceTextKeepsStyleSpans(sourceText)
96+ assertThat(nodeInfo.text.toString()).isEqualTo(" Read docs now" )
97+ assertAccessibilityTextDoesNotHaveVisualSpans(accessibilityText)
98+ assertPreservedSpanMatchesSource(sourceText, accessibilityText, clickableSpan)
99+ assertPreservedSpanMatchesSource(sourceText, accessibilityText, urlSpan)
72100 }
73101
74102 @Test
75- fun preparedLayoutTextViewAccessibilityNodeText_stripsStyleSpans () {
103+ fun preparedLayoutTextViewAccessibilityNodeText_stripsStyleSpansAndPreservesClickableSpan () {
76104 val text = createStyledText(" Prepared text" )
105+ val clickableSpan =
106+ object : ClickableSpan () {
107+ override fun onClick (widget : View ) = Unit
108+ }
109+ text.setSpan(clickableSpan, 0 , 8 , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
77110 val layout =
78111 StaticLayout .Builder .obtain(text, 0 , text.length, TextPaint (), 300 ).build()
79112 val textView = PreparedLayoutTextView (RuntimeEnvironment .getApplication())
@@ -96,7 +129,9 @@ class ReactTextViewAccessibilityDelegateTest {
96129
97130 assertSourceTextKeepsStyleSpans(textView.text)
98131 assertThat(nodeInfo.text.toString()).isEqualTo(" Prepared text" )
99- assertThat(nodeInfo.text).isNotInstanceOf(Spanned ::class .java)
132+ val accessibilityText = nodeInfo.text as Spanned
133+ assertAccessibilityTextDoesNotHaveVisualSpans(accessibilityText)
134+ assertPreservedSpanMatchesSource(textView.text as Spanned , accessibilityText, clickableSpan)
100135 }
101136
102137 private fun createReactTextViewWithStyledText (text : String ): ReactTextView {
@@ -126,6 +161,8 @@ class ReactTextViewAccessibilityDelegateTest {
126161 SpannableString (text).apply {
127162 setSpan(AbsoluteSizeSpan (48 ), 0 , length, Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
128163 setSpan(ForegroundColorSpan (Color .BLACK ), 0 , length, Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
164+ setSpan(BackgroundColorSpan (Color .WHITE ), 0 , length, Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
165+ setSpan(StyleSpan (android.graphics.Typeface .BOLD ), 0 , length, Spanned .SPAN_EXCLUSIVE_EXCLUSIVE )
129166 }
130167
131168 private fun createNodeInfo (view : View ): AccessibilityNodeInfoCompat =
@@ -138,5 +175,37 @@ class ReactTextViewAccessibilityDelegateTest {
138175 val spanned = text as Spanned
139176 assertThat(spanned.getSpans(0 , spanned.length, AbsoluteSizeSpan ::class .java)).isNotEmpty()
140177 assertThat(spanned.getSpans(0 , spanned.length, ForegroundColorSpan ::class .java)).isNotEmpty()
178+ assertThat(spanned.getSpans(0 , spanned.length, BackgroundColorSpan ::class .java)).isNotEmpty()
179+ assertThat(spanned.getSpans(0 , spanned.length, StyleSpan ::class .java)).isNotEmpty()
180+ }
181+
182+ private fun assertAccessibilityTextDoesNotHaveVisualSpans (text : CharSequence? ) {
183+ if (text !is Spanned ) {
184+ return
185+ }
186+
187+ assertThat(text.getSpans(0 , text.length, AbsoluteSizeSpan ::class .java)).isEmpty()
188+ assertThat(text.getSpans(0 , text.length, ForegroundColorSpan ::class .java)).isEmpty()
189+ assertThat(text.getSpans(0 , text.length, BackgroundColorSpan ::class .java)).isEmpty()
190+ assertThat(text.getSpans(0 , text.length, StyleSpan ::class .java)).isEmpty()
191+ }
192+
193+ private fun assertPreservedSpanMatchesSource (
194+ sourceText : Spanned ,
195+ accessibilityText : Spanned ,
196+ sourceSpan : Any ,
197+ ) {
198+ val preservedSpans =
199+ 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+
209+ assertThat(preservedSpans).isNotEmpty()
141210 }
142211}
0 commit comments