From 3f3030af4a0a6c93e0e739465018eafc56082814 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Mon, 28 Sep 2026 19:33:03 -0700 Subject: [PATCH 1/2] fix(android): crash when setting a percentage borderRadius on Text, TextInput, and ScrollView (#57869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Follow-up to https://github.com/react/react-native/issues/57795, which fixed the percentage-`borderRadius` crash for `` (https://github.com/react/react-native/issues/53977). The same latent bug existed in every remaining Android view manager whose `borderRadius` `ReactPropGroup` setter was still typed as `Float`: - `ReactTextViewManager` (``) - `PreparedLayoutTextViewManager` (internal, prepared-layout ``) - `ReactTextInputManager` (``) - `ReactScrollViewManager` (``) - `ReactHorizontalScrollViewManager` (``) - `ReactNestedScrollViewManager` (internal, nested-scroll variant) Percentage border radii arrive from JS as strings (`'50%'`), so the reflection-based property updater throws: ``` com.facebook.react.bridge.JSApplicationIllegalArgumentException: Error while updating property 'borderRadius' of a view managed by: RCTText Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double ``` Each setter now accepts a `Dynamic` parsed with `LengthPercentage.setFromDynamic`, completing the migration `ReactViewManager` received in 0.75 and `ReactImageManager` in https://github.com/react/react-native/issues/57795. The `Float` overloads on the four public managers are kept as deprecated pass-throughs for source/binary compatibility (public API dump updated); the two internal managers are migrated outright. Rendering needs no changes since all six managers already delegate to `BackgroundStyleApplicator`, which resolves percentages. ## Changelog: [ANDROID] [FIXED] - Fix crash when setting a percentage borderRadius on Text, TextInput, and ScrollView Pull Request resolved: https://github.com/react/react-native/pull/57869 Test Plan: **Unit tests** — new Robolectric regression tests mirroring the one merged in https://github.com/react/react-native/issues/57795: - `ReactTextViewPropertyTest.testBorderRadius` (new file) - `ReactTextInputPropertyTest.testBorderRadius` Both verified red before the fix (failing with `JSApplicationIllegalArgumentException: Error while updating property 'borderRadius' of a view managed by: RCTText`) and green after (`:packages:react-native:ReactAndroid:testDebugUnitTest`, 25/25 passing). **Manual QA** — rn-tester on an Android emulator (API 35), rendering the snippet below with `borderRadius: '50%'` on `` and `'20%'` on ``, ``, and ``: ```jsx Text 50% ... ... ``` On main this screen crashes with the exception above; with this change all four render rounded corners. ### Screenshots | Before (main) — surface fails to mount, exception above in logcat | After (this PR) — all four components render their percentage radii | | --- | --- | | | | Differential Revision: D122262619 Pulled By: Abbondanzo --- .../ReactAndroid/api/ReactAndroid.api | 4 + .../ReactHorizontalScrollViewManager.kt | 17 +++- .../scroll/ReactNestedScrollViewManager.kt | 15 +++- .../views/scroll/ReactScrollViewManager.kt | 13 ++- .../text/PreparedLayoutTextViewManager.kt | 11 +-- .../react/views/text/ReactTextViewManager.kt | 19 +++-- .../views/textinput/ReactTextInputManager.kt | 18 +++-- .../views/text/ReactTextViewPropertyTest.kt | 81 +++++++++++++++++++ .../textinput/ReactTextInputPropertyTest.kt | 20 +++++ 9 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewPropertyTest.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f4b235d3f321..29a04dfbfc07 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5583,6 +5583,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager : public synthetic fun scrollToEnd (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V public final fun setBorderColor (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;ILjava/lang/Integer;)V public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;IF)V + public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;ILcom/facebook/react/bridge/Dynamic;)V public final fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;Ljava/lang/String;)V public final fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;IF)V public final fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;I)V @@ -5878,6 +5879,7 @@ public class com/facebook/react/views/scroll/ReactScrollViewManager : com/facebo public synthetic fun scrollToEnd (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V public final fun setBorderColor (Lcom/facebook/react/views/scroll/ReactScrollView;ILjava/lang/Integer;)V public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V + public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;ILcom/facebook/react/bridge/Dynamic;)V public final fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V public final fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V public final fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactScrollView;I)V @@ -6073,6 +6075,7 @@ public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/r public final fun setAndroidHyphenationFrequency (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V public final fun setBorderColor (Lcom/facebook/react/views/text/ReactTextView;ILjava/lang/Integer;)V public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;IF)V + public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;ILcom/facebook/react/bridge/Dynamic;)V public final fun setBorderStyle (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V public final fun setBorderWidth (Lcom/facebook/react/views/text/ReactTextView;IF)V public final fun setDataDetectorType (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V @@ -6337,6 +6340,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face public final fun setAutoFocus (Lcom/facebook/react/views/textinput/ReactEditText;Z)V public final fun setBorderColor (Lcom/facebook/react/views/textinput/ReactEditText;ILjava/lang/Integer;)V public final fun setBorderRadius (Lcom/facebook/react/views/textinput/ReactEditText;IF)V + public final fun setBorderRadius (Lcom/facebook/react/views/textinput/ReactEditText;ILcom/facebook/react/bridge/Dynamic;)V public final fun setBorderStyle (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V public final fun setBorderWidth (Lcom/facebook/react/views/textinput/ReactEditText;IF)V public final fun setCaretHidden (Lcom/facebook/react/views/textinput/ReactEditText;Z)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt index bac81c404c1c..b361a03f3e53 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt @@ -282,9 +282,24 @@ constructor(private val fpsListener: FpsListener? = null) : ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, + ) + public fun setBorderRadius( + view: ReactHorizontalScrollView?, + index: Int, + rawBorderRadius: Dynamic, + ) { + if (view != null) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius) + } + } + + @Deprecated( + "Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.", ) public fun setBorderRadius(view: ReactHorizontalScrollView?, index: Int, borderRadius: Float) { + // Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic + // would not map NaN back to null like the original Float path did. if (view != null) { val radius = if (borderRadius.isNaN()) null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt index d86d46a29d34..50ec11cbe1e1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<3b339a7d81f8e32cf6b6ca93ae9a388b>> */ /** @@ -255,9 +255,20 @@ constructor(private val fpsListener: FpsListener? = null) : ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, + ) + fun setBorderRadius(view: ReactNestedScrollView?, index: Int, rawBorderRadius: Dynamic) { + if (view != null) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius) + } + } + + @Deprecated( + "Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.", ) fun setBorderRadius(view: ReactNestedScrollView?, index: Int, borderRadius: Float) { + // Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic + // would not map NaN back to null like the original Float path did. if (view != null) { val radius = if (borderRadius.isNaN()) null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt index 99cce0dbe809..bcf995715677 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt @@ -247,9 +247,20 @@ constructor(private val fpsListener: FpsListener? = null) : ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, + ) + public fun setBorderRadius(view: ReactScrollView?, index: Int, rawBorderRadius: Dynamic) { + if (view != null) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius) + } + } + + @Deprecated( + "Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.", ) public fun setBorderRadius(view: ReactScrollView?, index: Int, borderRadius: Float) { + // Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic + // would not map NaN back to null like the original Float path did. if (view != null) { val radius = if (borderRadius.isNaN()) null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt index e0ca6f1fe287..036695919522 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt @@ -13,6 +13,7 @@ import android.text.Spannable import android.text.Spanned import android.view.View import com.facebook.react.R +import com.facebook.react.bridge.Dynamic import com.facebook.react.internal.SystraceSection import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -20,7 +21,6 @@ import com.facebook.react.uimanager.BaseViewManager import com.facebook.react.uimanager.IViewGroupManager import com.facebook.react.uimanager.LayoutShadowNode import com.facebook.react.uimanager.LengthPercentage -import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.PointerEvents import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.ReferenceStateWrapper @@ -149,13 +149,10 @@ internal class PreparedLayoutTextViewManager : ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, ) - fun setBorderRadius(view: PreparedLayoutTextView, index: Int, borderRadius: Float) { - val radius = - if (borderRadius.isNaN()) null - else LengthPercentage(borderRadius, LengthPercentageType.POINT) - BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius) + fun setBorderRadius(view: PreparedLayoutTextView, index: Int, rawBorderRadius: Dynamic) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius) } @ReactProp(name = "borderStyle") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index eda2036c2b4d..044fa7923c6a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -19,6 +19,7 @@ import android.text.util.Linkify import android.view.Gravity import com.facebook.common.logging.FLog import com.facebook.react.R +import com.facebook.react.bridge.Dynamic import com.facebook.react.common.ReactConstants import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.mapbuffer.MapBuffer @@ -335,15 +336,21 @@ public constructor( ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, + ) + public fun setBorderRadius(view: ReactTextView, index: Int, rawBorderRadius: Dynamic) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius) + } + + @Deprecated( + "Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.", ) public fun setBorderRadius(view: ReactTextView, index: Int, borderRadius: Float) { + // Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic + // would not map NaN back to null like the original Float path did. val radius = - if (borderRadius.isNaN()) { - null - } else { - LengthPercentage(borderRadius, LengthPercentageType.POINT) - } + if (borderRadius.isNaN()) null + else LengthPercentage(borderRadius, LengthPercentageType.POINT) BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index f59c9b7f1e19..2830fc92a9f0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -807,15 +807,21 @@ public open class ReactTextInputManager public constructor() : ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ], - defaultFloat = Float.NaN, + ) + public fun setBorderRadius(view: ReactEditText, index: Int, rawBorderRadius: Dynamic) { + val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius) + setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius) + } + + @Deprecated( + "Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.", ) public fun setBorderRadius(view: ReactEditText, index: Int, borderRadius: Float) { + // Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic + // would not map NaN back to null like the original Float path did. val radius = - if (borderRadius.isNaN()) { - null - } else { - LengthPercentage(borderRadius, LengthPercentageType.POINT) - } + if (borderRadius.isNaN()) null + else LengthPercentage(borderRadius, LengthPercentageType.POINT) setBorderRadius(view, BorderRadiusProp.entries[index], radius) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewPropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewPropertyTest.kt new file mode 100644 index 000000000000..12adce0d1308 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewPropertyTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// TODO T207169925: Migrate CatalystInstance to Reacthost and remove the Suppress("DEPRECATION") +// annotation +@file:Suppress("DEPRECATION") + +package com.facebook.react.views.text + +import android.util.DisplayMetrics +import com.facebook.react.bridge.BridgeReactContext +import com.facebook.react.bridge.CatalystInstance +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.style.BorderRadiusProp +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +/** Verify view properties are being applied correctly by [ReactTextViewManager] */ +@RunWith(RobolectricTestRunner::class) +class ReactTextViewPropertyTest { + + private lateinit var context: BridgeReactContext + private lateinit var catalystInstanceMock: CatalystInstance + private lateinit var themedContext: ThemedReactContext + private lateinit var manager: ReactTextViewManager + + @Before + fun setup() { + ReactNativeFeatureFlagsForTests.setUp() + context = BridgeReactContext(RuntimeEnvironment.getApplication()) + catalystInstanceMock = createMockCatalystInstance() + context.initializeWithInstance(catalystInstanceMock) + themedContext = ThemedReactContext(context, context, null, -1) + manager = ReactTextViewManager() + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics()) + } + + @After + fun teardown() { + DisplayMetricsHolder.setScreenDisplayMetrics(null) + } + + private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap { + return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues)) + } + + @Test + fun testBorderRadius() { + val view = manager.createViewInstance(themedContext) + + // Percentage border radii arrive as strings and must not crash the property updater + manager.updateProperties(view, buildStyles("borderRadius", "50%")) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(50f, LengthPercentageType.PERCENT)) + + manager.updateProperties(view, buildStyles("borderRadius", 10.0)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(10f, LengthPercentageType.POINT)) + + manager.updateProperties(view, buildStyles("borderRadius", null)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isNull() + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt index 69a27794918d..216dd5522be9 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt @@ -32,9 +32,13 @@ import com.facebook.react.bridge.CatalystInstance import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorHint import com.facebook.react.views.text.ReactTextUpdate import com.facebook.react.views.text.internal.span.CustomStyleSpan @@ -635,6 +639,22 @@ class ReactTextInputPropertyTest { assertThat(checkNotNull(view.text).getSpans(0, view.length(), MarkerSpan::class.java)).isEmpty() } + @Test + fun testBorderRadius() { + // Percentage border radii arrive as strings and must not crash the property updater + manager.updateProperties(view, buildStyles("borderRadius", "50%")) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(50f, LengthPercentageType.PERCENT)) + + manager.updateProperties(view, buildStyles("borderRadius", 10.0)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(10f, LengthPercentageType.POINT)) + + manager.updateProperties(view, buildStyles("borderRadius", null)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isNull() + } + private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap { return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues)) } From b60db0f9d4e08af48825073fb4848b4279c96a46 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Tue, 29 Sep 2026 07:50:55 -0700 Subject: [PATCH 2/2] Cover percentage border radii on Android ScrollViews Summary: Add property-updater regression coverage for vertical and horizontal Android ScrollViews. Exercise every supported radius index, point values, percentages, negative values, null resets, and the deprecated Float overload. Changelog: [Internal] Differential Revision: D122278945 --- .../scroll/ReactScrollViewPropertyTest.kt | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewPropertyTest.kt diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewPropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewPropertyTest.kt new file mode 100644 index 000000000000..78fd8948678b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewPropertyTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// The deprecated APIs exercised here remain public and required by this Robolectric setup. +@file:Suppress("DEPRECATION") + +package com.facebook.react.views.scroll + +import android.annotation.SuppressLint +import android.util.DisplayMetrics +import android.view.View +import com.facebook.react.bridge.BridgeReactContext +import com.facebook.react.bridge.CatalystInstance +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.style.BorderRadiusProp +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +/** Verifies border radius properties applied by the vertical and horizontal ScrollView managers. */ +@SuppressLint("DeprecatedClass", "DeprecatedMethod") +@RunWith(RobolectricTestRunner::class) +class ReactScrollViewPropertyTest { + + private lateinit var context: BridgeReactContext + private lateinit var catalystInstanceMock: CatalystInstance + private lateinit var themedContext: ThemedReactContext + + @Before + fun setup() { + ReactNativeFeatureFlagsForTests.setUp() + context = BridgeReactContext(RuntimeEnvironment.getApplication()) + catalystInstanceMock = createMockCatalystInstance() + context.initializeWithInstance(catalystInstanceMock) + themedContext = ThemedReactContext(context, context, null, -1) + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics()) + } + + @After + fun teardown() { + DisplayMetricsHolder.setScreenDisplayMetrics(null) + } + + @Test + fun testVerticalBorderRadius() { + val manager = ReactScrollViewManager() + val view = manager.createViewInstance(themedContext) + + assertBorderRadiusUpdates(view) { manager.updateProperties(view, it) } + assertDeprecatedFloatBorderRadius(view) { index, radius -> + manager.setBorderRadius(view, index, radius) + } + } + + @Test + fun testHorizontalBorderRadius() { + val manager = ReactHorizontalScrollViewManager() + val view = manager.createViewInstance(themedContext) + + assertBorderRadiusUpdates(view) { manager.updateProperties(view, it) } + assertDeprecatedFloatBorderRadius(view) { index, radius -> + manager.setBorderRadius(view, index, radius) + } + } + + private fun assertBorderRadiusUpdates( + view: View, + updateProperties: (ReactStylesDiffMap) -> Unit, + ) { + val percentageRadii = + listOf( + Triple("borderRadius", BorderRadiusProp.BORDER_RADIUS, 10f), + Triple("borderTopLeftRadius", BorderRadiusProp.BORDER_TOP_LEFT_RADIUS, 20f), + Triple("borderTopRightRadius", BorderRadiusProp.BORDER_TOP_RIGHT_RADIUS, 30f), + Triple("borderBottomRightRadius", BorderRadiusProp.BORDER_BOTTOM_RIGHT_RADIUS, 40f), + Triple("borderBottomLeftRadius", BorderRadiusProp.BORDER_BOTTOM_LEFT_RADIUS, 50f), + ) + + percentageRadii.forEach { (name, property, value) -> + updateProperties(buildStyles(name, "$value%")) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, property)) + .isEqualTo(LengthPercentage(value, LengthPercentageType.PERCENT)) + } + + updateProperties(buildStyles("borderRadius", 12.0)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(12f, LengthPercentageType.POINT)) + + updateProperties(buildStyles("borderRadius", -1.0)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isNull() + + updateProperties(buildStyles("borderRadius", 13.0)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(13f, LengthPercentageType.POINT)) + + updateProperties(buildStyles("borderRadius", null)) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isNull() + } + + private fun assertDeprecatedFloatBorderRadius( + view: View, + setBorderRadius: (Int, Float) -> Unit, + ) { + setBorderRadius(0, 8f) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isEqualTo(LengthPercentage(8f, LengthPercentageType.POINT)) + + setBorderRadius(0, Float.NaN) + assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS)) + .isNull() + } + + private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap = + ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues)) +}