From b58d49966b8008309bc9cb0262190c22521d1974 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Thu, 27 Aug 2026 10:46:21 +0300 Subject: [PATCH 1/3] feat: support React Native 0.87.1 Adapt native APIs, Fabric shared-element ordering, strict public types and modern ReactHost autolinking. Add compatibility CI and regression coverage, repair native test-host linkage, and clear existing lint errors. --- .buildkite/jobs/pipeline.android_rn_87.yml | 10 ++ .buildkite/jobs/pipeline.ios_rn_87.yml | 11 ++ .buildkite/pipeline.sh | 2 + .yarnrc.yml | 8 ++ android/build.gradle | 4 +- .../reactnativenavigation/TestApplication.kt | 4 +- .../react/JsDevReloadHandler.java | 18 +-- .../react/NavigationTurboModule.kt | 6 +- .../react/modal/ModalViewManager.kt | 2 +- .../reactnativenavigation/utils/Context.kt | 4 +- .../views/bottomtabs/BottomTabsContainer.kt | 6 +- .../element/TransitionAnimatorCreator.kt | 13 +- .../views/element/ViewHierarchyComparator.kt | 31 ++++ .../animators/ReactImageMatrixAnimator.kt | 4 +- .../views/stack/topbar/titlebar/ButtonBar.kt | 17 ++- android/src/main/res/values/ids.xml | 3 +- .../reactnativenavigation/TestApplication.kt | 10 +- .../react/JsDevReloadHandlerTest.java | 65 +++++++++ .../utils/ContextTest.kt | 40 ++++++ .../modal/ModalPresenterTest.java | 8 +- .../viewcontroller/ViewControllerTest.java | 16 +-- .../element/TransitionAnimatorCreatorTest.kt | 89 ++++++++++++ .../element/ViewHierarchyComparatorTest.kt | 79 +++++++++++ .../fixtures/rn87/MainApplication.kt.template | 27 ++++ .../applicationLinker.test.js.snap | 2 +- autolink/postlink/applicationLinker.js | 25 +++- autolink/postlink/applicationLinker.test.js | 21 +++ ios/RNNAppDelegate.mm | 5 + ios/RNNEventEmitter.mm | 2 +- ios/RNNReactButtonView.h | 2 +- ios/RNNReactView.mm | 3 +- ios/RNNViewLocation.mm | 1 - ios/ReactNativeNavigation.h | 2 +- ios/ScreenAnimationController.h | 2 +- ios/TurboModules/RNNTurboManager.mm | 2 +- jest-setup.js | 2 +- playground/android/app/build.gradle | 1 + .../playground/MainApplication.kt | 2 +- playground/android/build.gradle | 7 +- playground/android/gradle.properties | 3 + playground/e2e/DeepLinking.test.js | 18 +-- playground/e2e/Orientation.test.js | 9 +- playground/e2e/SharedElements.test.js | 26 ++++ playground/e2e/detox.config.js | 7 +- .../NavigationTests/RNNCommandsHandlerTest.mm | 12 +- .../RNNModalManagerEventHandlerTest.mm | 9 +- .../RNNRootViewControllerTest.mm | 8 +- playground/ios/Podfile | 42 ++---- .../ios/playground.xcodeproj/project.pbxproj | 134 +----------------- playground/src/app.ts | 13 -- .../withGestureHandlerRoot.test.tsx | 24 ++++ .../src/components/withGestureHandlerRoot.tsx | 18 +++ .../src/screens/BottomTabsStylingScreen.tsx | 3 - playground/src/screens/ButtonsScreen.tsx | 1 - .../src/screens/FirstBottomTabScreen.tsx | 3 +- playground/src/screens/FlatListScreen.tsx | 3 +- playground/src/screens/LifecycleButton.tsx | 3 +- playground/src/screens/LifecycleScreen.tsx | 9 +- playground/src/screens/OverlayScreen.tsx | 3 +- playground/src/screens/PushedScreen.tsx | 3 +- playground/src/screens/RoundedButton.tsx | 3 +- .../src/screens/StaticLifecycleOverlay.tsx | 3 +- playground/src/screens/Toast.tsx | 3 +- playground/src/screens/index.tsx | 8 +- .../sharedElementCarDealer/CarStoryScreen.tsx | 2 - playground/src/services/Alert.ts | 11 ++ scripts/changeReactNativeVersion.js | 6 + scripts/ci.sh | 5 +- scripts/codegenCompatibility.test.js | 12 ++ scripts/test-unit.js | 7 +- scripts/versionMapping.js | 14 ++ scripts/versionMapping.test.js | 20 +++ src/adapters/CodegenTypes.ts | 5 + src/adapters/NativeEventsReceiver.test.ts | 34 +++++ src/adapters/NativeEventsReceiver.ts | 38 ++--- src/adapters/NativeRNNTurboEventEmitter.ts | 7 +- src/adapters/NativeRNNTurboModule.ts | 2 +- src/adapters/TouchablePreview.tsx | 5 +- src/components/Modal.tsx | 25 ++-- src/interfaces/Options.ts | 5 +- tsconfig.json | 3 +- website/docs/docs/docs-Installing.mdx | 26 +++- 82 files changed, 791 insertions(+), 360 deletions(-) create mode 100644 .buildkite/jobs/pipeline.android_rn_87.yml create mode 100644 .buildkite/jobs/pipeline.ios_rn_87.yml create mode 100644 android/src/main/java/com/reactnativenavigation/views/element/ViewHierarchyComparator.kt create mode 100644 android/src/test/java/com/reactnativenavigation/react/JsDevReloadHandlerTest.java create mode 100644 android/src/test/java/com/reactnativenavigation/utils/ContextTest.kt create mode 100644 android/src/test/java/com/reactnativenavigation/views/element/TransitionAnimatorCreatorTest.kt create mode 100644 android/src/test/java/com/reactnativenavigation/views/element/ViewHierarchyComparatorTest.kt create mode 100644 autolink/fixtures/rn87/MainApplication.kt.template create mode 100644 playground/e2e/SharedElements.test.js create mode 100644 playground/src/components/withGestureHandlerRoot.test.tsx create mode 100644 playground/src/components/withGestureHandlerRoot.tsx create mode 100644 playground/src/services/Alert.ts create mode 100644 scripts/codegenCompatibility.test.js create mode 100644 scripts/versionMapping.test.js create mode 100644 src/adapters/CodegenTypes.ts create mode 100644 src/adapters/NativeEventsReceiver.test.ts diff --git a/.buildkite/jobs/pipeline.android_rn_87.yml b/.buildkite/jobs/pipeline.android_rn_87.yml new file mode 100644 index 00000000000..2fe850d8751 --- /dev/null +++ b/.buildkite/jobs/pipeline.android_rn_87.yml @@ -0,0 +1,10 @@ + - label: ":android: Android (RN 0.87.1)" + env: + JAVA_HOME: /opt/openjdk/jdk-17.0.9.jdk/Contents/Home/ + REACT_NATIVE_VERSION: 0.87.1 + command: + - "nvm install" + - "./scripts/ci.android.sh" + key: "android_rn_87" + timeout_in_minutes: 60 + artifact_paths: "/Users/builder/uibuilder/work/playground/artifacts/**/*" diff --git a/.buildkite/jobs/pipeline.ios_rn_87.yml b/.buildkite/jobs/pipeline.ios_rn_87.yml new file mode 100644 index 00000000000..1e95410fee1 --- /dev/null +++ b/.buildkite/jobs/pipeline.ios_rn_87.yml @@ -0,0 +1,11 @@ + - label: ":ios: iOS (RN 0.87.1)" + env: + REACT_NATIVE_VERSION: 0.87.1 + IOS_TEST_DEVICE: iPhone 17 Pro Max + IOS_TEST_OS: "26.1" + command: + - "nvm install" + - "./scripts/ci.ios.sh" + key: "ios_rn_87" + timeout_in_minutes: 60 + artifact_paths: "/Users/builder/uibuilder/work/playground/artifacts/**/*" diff --git a/.buildkite/pipeline.sh b/.buildkite/pipeline.sh index b9b14330b9a..c3ddcbadd83 100755 --- a/.buildkite/pipeline.sh +++ b/.buildkite/pipeline.sh @@ -7,8 +7,10 @@ cat .buildkite/jobs/pipeline.android_rn_77.yml cat .buildkite/jobs/pipeline.android_rn_78.yml cat .buildkite/jobs/pipeline.android_rn_84.yml cat .buildkite/jobs/pipeline.android_rn_85.yml +cat .buildkite/jobs/pipeline.android_rn_87.yml cat .buildkite/jobs/pipeline.ios_rn_77.yml cat .buildkite/jobs/pipeline.ios_rn_78.yml cat .buildkite/jobs/pipeline.ios_rn_84.yml cat .buildkite/jobs/pipeline.ios_rn_85.yml +cat .buildkite/jobs/pipeline.ios_rn_87.yml cat .buildkite/jobs/pipeline.publish.yml diff --git a/.yarnrc.yml b/.yarnrc.yml index 237c13dcfcd..bdf1ca147f5 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -5,6 +5,14 @@ enableGlobalCache: false nmHoistingLimits: workspaces npmMinimalAgeGate: 14d +# Exact versions exercised by the RN 0.87 compatibility jobs. +npmPreapprovedPackages: + - "react-native@0.87.1" + - "@react-native/*@0.87.1" + - "hermes-compiler@250829098.0.17" + - "react-native-gesture-handler@3.2.1" + - "react-native-reanimated@4.6.0" + - "react-native-worklets@0.12.1" nodeLinker: node-modules npmRegistryServer: "https://registry.npmjs.org" diff --git a/android/build.gradle b/android/build.gradle index bec9808bfbf..2c73a022ead 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat buildscript { ext { - kotlinVersion = "2.1.20" + kotlinVersion = rootProject.ext.has('kotlinVersion') ? rootProject.ext.kotlinVersion : "2.1.20" RNNKotlinVersion = kotlinVersion detoxKotlinVersion = kotlinVersion compileSdkVersion = 36 @@ -79,7 +79,7 @@ android { minifyEnabled false } } - lintOptions { + lint { abortOnError false } diff --git a/android/src/androidTest/java/com/reactnativenavigation/TestApplication.kt b/android/src/androidTest/java/com/reactnativenavigation/TestApplication.kt index aec08e356c2..21e1fe9763b 100644 --- a/android/src/androidTest/java/com/reactnativenavigation/TestApplication.kt +++ b/android/src/androidTest/java/com/reactnativenavigation/TestApplication.kt @@ -10,7 +10,7 @@ import com.reactnativenavigation.react.NavigationReactNativeHost class TestApplication : NavigationApplication() { override val reactNativeHost: ReactNativeHost - get() = object : NavigationReactNativeHost(this) { + get() = object : NavigationReactNativeHost(this@TestApplication) { override fun getJSMainModuleName(): String { return "index" } @@ -34,4 +34,4 @@ class TestApplication : NavigationApplication() { override val reactHost: ReactHost get() = getDefaultReactHost(this, reactNativeHost) -} \ No newline at end of file +} diff --git a/android/src/main/java/com/reactnativenavigation/react/JsDevReloadHandler.java b/android/src/main/java/com/reactnativenavigation/react/JsDevReloadHandler.java index c562ccd5511..84a422cf5a7 100644 --- a/android/src/main/java/com/reactnativenavigation/react/JsDevReloadHandler.java +++ b/android/src/main/java/com/reactnativenavigation/react/JsDevReloadHandler.java @@ -7,7 +7,7 @@ import android.content.IntentFilter; import android.view.KeyEvent; import android.widget.EditText; -import android.os.Build; +import androidx.core.content.ContextCompat; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.reactnativenavigation.utils.UiUtils; @@ -19,6 +19,8 @@ public interface ReloadListener { void onReload(); } + private static final ReloadListener NO_OP_RELOAD_LISTENER = () -> {}; + private final BroadcastReceiver reloadReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { @@ -28,7 +30,7 @@ public void onReceive(final Context context, final Intent intent) { private final DevSupportManager devSupportManager; private long firstRTimestamp = 0; - private ReloadListener reloadListener = () -> {}; + private ReloadListener reloadListener = NO_OP_RELOAD_LISTENER; JsDevReloadHandler(DevSupportManager devSupportManager) { this.devSupportManager = devSupportManager; @@ -36,7 +38,7 @@ public void onReceive(final Context context, final Intent intent) { @Override public void onSuccess() { - UiUtils.runOnMainThread(reloadListener::onReload); + UiUtils.runOnMainThread(() -> reloadListener.onReload()); } public void setReloadListener(ReloadListener listener) { @@ -45,16 +47,14 @@ public void setReloadListener(ReloadListener listener) { public void removeReloadListener(ReloadListener listener) { if (reloadListener == listener) { - reloadListener = null; + reloadListener = NO_OP_RELOAD_LISTENER; } } public void onActivityResumed(Activity activity) { - if (Build.VERSION.SDK_INT >= 34 && activity.getApplicationInfo().targetSdkVersion >= 34) { - activity.registerReceiver(reloadReceiver, new IntentFilter(RELOAD_BROADCAST), Context.RECEIVER_EXPORTED); - } else { - activity.registerReceiver(reloadReceiver, new IntentFilter(RELOAD_BROADCAST)); - } + // Exported intentionally so the development reload broadcast can be sent by adb. + ContextCompat.registerReceiver(activity, reloadReceiver, + new IntentFilter(RELOAD_BROADCAST), ContextCompat.RECEIVER_EXPORTED); } public void onActivityPaused(Activity activity) { diff --git a/android/src/main/java/com/reactnativenavigation/react/NavigationTurboModule.kt b/android/src/main/java/com/reactnativenavigation/react/NavigationTurboModule.kt index de36aa609a5..2e7182248d1 100644 --- a/android/src/main/java/com/reactnativenavigation/react/NavigationTurboModule.kt +++ b/android/src/main/java/com/reactnativenavigation/react/NavigationTurboModule.kt @@ -65,7 +65,7 @@ class NavigationTurboModule( UiUtils.getBottomTabsHeight(reactApplicationContext).toFloat() ).toDouble() constants[Constants.STATUS_BAR_HEIGHT_KEY] = - UiUtils.pxToDp(reactApplicationContext, getStatusBarHeight(currentActivity).toFloat()) + UiUtils.pxToDp(reactApplicationContext, getStatusBarHeight(getCurrentActivity()).toFloat()) .toDouble() constants[Constants.TOP_BAR_HEIGHT_KEY] = UiUtils.pxToDp( reactApplicationContext, @@ -84,7 +84,7 @@ class NavigationTurboModule( handle { Log.d("NavigationTurboModule", "setRoot handle ${Thread.currentThread()}") val viewController = layoutFactory.create(layoutTree) - val activity = currentActivity + val activity = getCurrentActivity() if (activity == null) { promise.reject("ACTIVITY_NULL", "Activity is null") return@handle @@ -299,7 +299,7 @@ class NavigationTurboModule( if (activity == null) { Log.e("NavigationTurboModule", "current activity is null!") } - return currentActivity as NavigationActivity? + return activity } companion object { diff --git a/android/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt b/android/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt index 2ab4afcd71e..9e923dfdaa5 100644 --- a/android/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt +++ b/android/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt @@ -141,7 +141,7 @@ private fun getModalHostSize(activity: Activity): Point { private class ModalHostShadowNode : LayoutShadowNode() { override fun addChildAt(child: ReactShadowNodeImpl, i: Int) { super.addChildAt(child, i) - themedContext?.currentActivity?.let { + getThemedContext()?.currentActivity?.let { val modalSize = getModalHostSize(it) child.setStyleWidth(modalSize.x.toFloat()) child.setStyleHeight(modalSize.y.toFloat()) diff --git a/android/src/main/java/com/reactnativenavigation/utils/Context.kt b/android/src/main/java/com/reactnativenavigation/utils/Context.kt index adee027a423..e4e5cb2b69e 100644 --- a/android/src/main/java/com/reactnativenavigation/utils/Context.kt +++ b/android/src/main/java/com/reactnativenavigation/utils/Context.kt @@ -7,7 +7,7 @@ import com.facebook.react.ReactApplication import com.reactnativenavigation.NavigationApplication fun Context.isDebug(): Boolean { - return (applicationContext as ReactApplication).reactNativeHost.useDeveloperSupport + return (applicationContext as ReactApplication).reactHost?.devSupportManager?.devSupportEnabled == true } fun isDarkMode() = NavigationApplication.instance.isDarkMode() fun Context.isDarkMode(): Boolean = when (AppCompatDelegate.getDefaultNightMode()) { @@ -16,4 +16,4 @@ fun Context.isDarkMode(): Boolean = when (AppCompatDelegate.getDefaultNightMode( else -> resources.configuration.isDarkMode() } fun Configuration.isDarkMode() = - (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES \ No newline at end of file + (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES diff --git a/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabsContainer.kt b/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabsContainer.kt index 3a937ac6f0b..480801b6f22 100644 --- a/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabsContainer.kt +++ b/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabsContainer.kt @@ -10,7 +10,7 @@ import android.view.ViewOutlineProvider import android.widget.FrameLayout.LayoutParams.MATCH_PARENT import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT import android.widget.LinearLayout -import androidx.annotation.RestrictTo +import androidx.annotation.VisibleForTesting import androidx.core.graphics.ColorUtils import androidx.core.view.updatePadding import com.reactnativenavigation.options.params.Fraction @@ -43,8 +43,8 @@ class BottomTabsContainer(context: Context, val bottomTabs: BottomTabs) : Shadow private var blurColor: Int? = null var topOutLineView = TopOutlineView(context) - @RestrictTo(RestrictTo.Scope.TESTS, RestrictTo.Scope.SUBCLASSES) get - @RestrictTo(RestrictTo.Scope.TESTS) set(value) { + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) get + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) set(value) { this.removeView(field) addView(value, LayoutParams(MATCH_PARENT, WRAP_CONTENT)) field = value diff --git a/android/src/main/java/com/reactnativenavigation/views/element/TransitionAnimatorCreator.kt b/android/src/main/java/com/reactnativenavigation/views/element/TransitionAnimatorCreator.kt index f8fb69419fb..950c3e9af29 100644 --- a/android/src/main/java/com/reactnativenavigation/views/element/TransitionAnimatorCreator.kt +++ b/android/src/main/java/com/reactnativenavigation/views/element/TransitionAnimatorCreator.kt @@ -10,7 +10,6 @@ import android.widget.FrameLayout import androidx.core.animation.doOnCancel import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart -import com.facebook.react.uimanager.ViewGroupManager import com.reactnativenavigation.R import com.reactnativenavigation.options.AnimationOptions import com.reactnativenavigation.options.LayoutAnimation @@ -61,7 +60,9 @@ open class TransitionAnimatorCreator @JvmOverloads constructor(private val trans private fun reparentViews(transitions: TransitionSet) { transitions.transitions - .sortedBy { getZIndex(it.view) } + // Sort before detaching any views: Fabric's native hierarchy is the + // source of truth for z-order, including transitions in nested parents. + .sortedWith { first, second -> ViewHierarchyComparator.compare(first.view, second.view) } .forEach { reparent(it) } transitions.validSharedElementTransitions .forEach { it.view.visibility = View.INVISIBLE } @@ -99,7 +100,6 @@ open class TransitionAnimatorCreator @JvmOverloads constructor(private val trans mutableListOf().apply { addAll(transitions.validSharedElementTransitions) addAll(transitions.validElementTransitions) - sortBy { getZIndex(it.view) } sortBy { it.view.getTag(R.id.original_index_in_parent) as Int } forEach { removeFromOverlay(it.viewController, it.view) @@ -124,7 +124,6 @@ open class TransitionAnimatorCreator @JvmOverloads constructor(private val trans view.setTag(R.id.original_left, view.left) view.setTag(R.id.original_pivot_x, view.pivotX) view.setTag(R.id.original_pivot_y, view.pivotY) - view.setTag(R.id.original_z_index, getZIndex(view)) biologicalParent.removeView(view) @@ -152,10 +151,6 @@ open class TransitionAnimatorCreator @JvmOverloads constructor(private val trans parent.addView(element, index, lp) } - private fun getZIndex(view: View) = ViewGroupManager.getViewZIndex(view) - ?: ViewTags.get(view, R.id.original_z_index) - ?: 0 - private fun addToOverlay(vc: ViewController<*>, element: View, lp: FrameLayout.LayoutParams) { val viewController = vc.parentController ?: vc viewController.addOverlay(element, lp) @@ -165,4 +160,4 @@ open class TransitionAnimatorCreator @JvmOverloads constructor(private val trans val viewController = vc.parentController ?: vc viewController.removeOverlay(element) } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/reactnativenavigation/views/element/ViewHierarchyComparator.kt b/android/src/main/java/com/reactnativenavigation/views/element/ViewHierarchyComparator.kt new file mode 100644 index 00000000000..c3b5da1abf3 --- /dev/null +++ b/android/src/main/java/com/reactnativenavigation/views/element/ViewHierarchyComparator.kt @@ -0,0 +1,31 @@ +package com.reactnativenavigation.views.element + +import android.view.View +import android.view.ViewGroup + +/** Fabric applies React zIndex by ordering native children, not by setting View.z. */ +internal object ViewHierarchyComparator : Comparator { + override fun compare(first: View, second: View): Int { + val firstPath = drawingPath(first) + val secondPath = drawingPath(second) + for (index in 0 until minOf(firstPath.size, secondPath.size)) { + val elevationOrder = firstPath[index].first.compareTo(secondPath[index].first) + if (elevationOrder != 0) return elevationOrder + val siblingOrder = firstPath[index].second.compareTo(secondPath[index].second) + if (siblingOrder != 0) return siblingOrder + } + return firstPath.size.compareTo(secondPath.size) + } + + private fun drawingPath(view: View): List> { + val path = mutableListOf>() + var child = view + var parent = child.parent as? ViewGroup + while (parent != null) { + path.add(child.z to parent.indexOfChild(child)) + child = parent + parent = child.parent as? ViewGroup + } + return path.asReversed() + } +} diff --git a/android/src/main/java/com/reactnativenavigation/views/element/animators/ReactImageMatrixAnimator.kt b/android/src/main/java/com/reactnativenavigation/views/element/animators/ReactImageMatrixAnimator.kt index 1d8f81ce235..ede390ae04c 100644 --- a/android/src/main/java/com/reactnativenavigation/views/element/animators/ReactImageMatrixAnimator.kt +++ b/android/src/main/java/com/reactnativenavigation/views/element/animators/ReactImageMatrixAnimator.kt @@ -27,14 +27,14 @@ class ReactImageMatrixAnimator(from: View, to: View) : PropertyAnimatorCreator 0) menu.clear() } @@ -84,4 +87,4 @@ open class ButtonBar internal constructor(context: Context) : Toolbar(context) { } } } -} \ No newline at end of file +} diff --git a/android/src/main/res/values/ids.xml b/android/src/main/res/values/ids.xml index f43a89d7454..a5e3aa8ed06 100644 --- a/android/src/main/res/values/ids.xml +++ b/android/src/main/res/values/ids.xml @@ -15,5 +15,4 @@ - - \ No newline at end of file + diff --git a/android/src/test/java/com/reactnativenavigation/TestApplication.kt b/android/src/test/java/com/reactnativenavigation/TestApplication.kt index 81e15dd3fd1..3e2de020159 100644 --- a/android/src/test/java/com/reactnativenavigation/TestApplication.kt +++ b/android/src/test/java/com/reactnativenavigation/TestApplication.kt @@ -6,10 +6,11 @@ import com.facebook.react.ReactApplication import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage +import com.facebook.react.devsupport.interfaces.DevSupportManager import org.mockito.Mockito class TestApplication : Application(), ReactApplication { - override val reactNativeHost: ReactNativeHost = object : ReactNativeHost(this) { + override val reactNativeHost: ReactNativeHost = object : ReactNativeHost(this@TestApplication) { override fun getUseDeveloperSupport(): Boolean { return true } @@ -24,6 +25,9 @@ class TestApplication : Application(), ReactApplication { setTheme(R.style.Theme_AppCompat) } - override val reactHost: ReactHost - get() = Mockito.mock(ReactHost::class.java) + override val reactHost: ReactHost = Mockito.mock(ReactHost::class.java).apply { + val manager = Mockito.mock(DevSupportManager::class.java) + Mockito.`when`(manager.devSupportEnabled).thenReturn(true) + Mockito.`when`(devSupportManager).thenReturn(manager) + } } diff --git a/android/src/test/java/com/reactnativenavigation/react/JsDevReloadHandlerTest.java b/android/src/test/java/com/reactnativenavigation/react/JsDevReloadHandlerTest.java new file mode 100644 index 00000000000..242cfd7525d --- /dev/null +++ b/android/src/test/java/com/reactnativenavigation/react/JsDevReloadHandlerTest.java @@ -0,0 +1,65 @@ +package com.reactnativenavigation.react; + +import android.app.Activity; +import android.content.Intent; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = {28, 33, 34}, manifest = Config.NONE) +public class JsDevReloadHandlerTest { + private Activity activity; + private DevSupportManager devSupportManager; + private JsDevReloadHandler handler; + + @Before + public void setUp() { + activity = Robolectric.buildActivity(Activity.class).setup().get(); + devSupportManager = mock(DevSupportManager.class); + handler = new JsDevReloadHandler(devSupportManager); + } + + @Test + public void resumedActivityReceivesDevelopmentReloadBroadcast() { + handler.onActivityResumed(activity); + activity.sendBroadcast(new Intent("com.reactnativenavigation.broadcast.RELOAD")); + ShadowLooper.idleMainLooper(); + + verify(devSupportManager).handleReloadJS(); + handler.onActivityPaused(activity); + } + + @Test + public void pausedActivityNoLongerReceivesReloadBroadcast() { + handler.onActivityResumed(activity); + handler.onActivityPaused(activity); + activity.sendBroadcast(new Intent("com.reactnativenavigation.broadcast.RELOAD")); + ShadowLooper.idleMainLooper(); + + verify(devSupportManager, never()).handleReloadJS(); + } + + @Test + public void removingListenerDoesNotCrashPendingReloadCallbacks() { + JsDevReloadHandler.ReloadListener listener = mock(JsDevReloadHandler.ReloadListener.class); + handler.setReloadListener(listener); + handler.removeReloadListener(listener); + handler.onSuccess(); + handler.onActivityResumed(activity); + activity.sendBroadcast(new Intent("com.reactnativenavigation.broadcast.RELOAD")); + ShadowLooper.idleMainLooper(); + + verify(listener, never()).onReload(); + verify(devSupportManager).handleReloadJS(); + handler.onActivityPaused(activity); + } +} diff --git a/android/src/test/java/com/reactnativenavigation/utils/ContextTest.kt b/android/src/test/java/com/reactnativenavigation/utils/ContextTest.kt new file mode 100644 index 00000000000..d13e59982c1 --- /dev/null +++ b/android/src/test/java/com/reactnativenavigation/utils/ContextTest.kt @@ -0,0 +1,40 @@ +package com.reactnativenavigation.utils + +import android.app.Application +import android.content.Context +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeHost +import com.facebook.react.devsupport.interfaces.DevSupportManager +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class ContextTest { + @Test + fun readsDeveloperSupportFromReactHostWithoutAccessingLegacyHost() { + val manager = mock() + val host = mock() + whenever(host.devSupportManager).thenReturn(manager) + val application = object : Application(), ReactApplication { + override val reactHost = host + override val reactNativeHost: ReactNativeHost + get() = error("ReactHost-only applications have no legacy host") + } + val context = mock() + whenever(context.applicationContext).thenReturn(application) + + whenever(manager.devSupportEnabled).thenReturn(true) + assertThat(context.isDebug()).isTrue() + whenever(manager.devSupportEnabled).thenReturn(false) + assertThat(context.isDebug()).isFalse() + whenever(host.devSupportManager).thenReturn(null) + assertThat(context.isDebug()).isFalse() + } +} diff --git a/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java b/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java index 200cdca6e26..a1bc86496ad 100644 --- a/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java +++ b/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java @@ -1,7 +1,6 @@ package com.reactnativenavigation.viewcontrollers.modal; import android.app.Activity; -import android.os.Looper; import android.widget.FrameLayout; import com.reactnativenavigation.BaseTest; @@ -23,7 +22,6 @@ import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; -import org.robolectric.Shadows; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -88,7 +86,7 @@ public void showModal_noAnimation() { CommandListener listener = spy(new CommandListenerAdapter() { @Override public void onSuccess(String childId) { - Shadows.shadowOf(Looper.getMainLooper()).idle(); + idleMainLooper(); assertThat(modal1.getView().getParent()).isEqualTo(modalsLayout); verify(modal1).onViewWillAppear(); } @@ -223,7 +221,7 @@ public void dismissModal_noAnimation() { disableModalAnimations(modal1); uut.showModal(modal1, root, new CommandListenerAdapter()); - Shadows.shadowOf(Looper.getMainLooper()).idle(); + idleMainLooper(); uut.dismissModal(modal1, root, root, new CommandListenerAdapter()); verify(modal1).onViewDisappear(); verify(modal1).destroy(); @@ -243,7 +241,7 @@ public void dismissModal_previousModalIsAddedBackToHierarchy() { idleMainLooper(); assertThat(modal1.getView().getParent()).isNull(); - Shadows.shadowOf(Looper.getMainLooper()).idle(); + idleMainLooper(); uut.dismissModal(modal2, modal1, root, new CommandListenerAdapter()); assertThat(modal1.getView().getParent()).isNotNull(); idleMainLooper(); diff --git a/android/src/test/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewControllerTest.java b/android/src/test/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewControllerTest.java index a4b48366a16..387a7872f36 100644 --- a/android/src/test/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewControllerTest.java +++ b/android/src/test/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewControllerTest.java @@ -28,7 +28,8 @@ import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; -import org.robolectric.Shadows; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowView; import java.lang.reflect.Field; @@ -188,7 +189,7 @@ public void onAppear_WhenShown() { Assertions.assertThat(spy.getView()).isNotShown(); verify(spy, times(0)).onViewWillAppear(); - Shadows.shadowOf(spy.getView()).setMyParent(mock(ViewParent.class)); + Shadow.extract(spy.getView()).setMyParent(mock(ViewParent.class)); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); Assertions.assertThat(spy.getView()).isShown(); @@ -198,7 +199,7 @@ public void onAppear_WhenShown() { @Test public void onAppear_CalledAtMostOnce() { ViewController spy = spy(uut); - Shadows.shadowOf(spy.getView()).setMyParent(mock(ViewParent.class)); + Shadow.extract(spy.getView()).setMyParent(mock(ViewParent.class)); Assertions.assertThat(spy.getView()).isShown(); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); @@ -216,7 +217,7 @@ public void isViewShown_doesNotCreateView() { @Test public void onDisappear_WhenNotShown_AfterOnAppearWasCalled() { ViewController spy = spy(uut); - Shadows.shadowOf(spy.getView()).setMyParent(mock(ViewParent.class)); + Shadow.extract(spy.getView()).setMyParent(mock(ViewParent.class)); Assertions.assertThat(spy.getView()).isShown(); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); verify(spy, times(1)).onViewWillAppear(); @@ -231,7 +232,7 @@ public void onDisappear_WhenNotShown_AfterOnAppearWasCalled() { @Test public void onDisappear_CalledAtMostOnce() { ViewController spy = spy(uut); - Shadows.shadowOf(spy.getView()).setMyParent(mock(ViewParent.class)); + Shadow.extract(spy.getView()).setMyParent(mock(ViewParent.class)); Assertions.assertThat(spy.getView()).isShown(); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); spy.getView().setVisibility(View.GONE); @@ -247,7 +248,7 @@ public void onDestroy_RemovesGlobalLayoutListener() throws Exception { ViewController spy = spy(uut); View view = spy.getView(); - Shadows.shadowOf(view).setMyParent(mock(ViewParent.class)); + Shadow.extract(view).setMyParent(mock(ViewParent.class)); spy.destroy(); @@ -264,7 +265,7 @@ public void onDestroy_RemovesGlobalLayoutListener() throws Exception { @Test public void onDestroy_CallsOnDisappearIfNeeded() { ViewController spy = spy(uut); - Shadows.shadowOf(spy.getView()).setMyParent(mock(ViewParent.class)); + Shadow.extract(spy.getView()).setMyParent(mock(ViewParent.class)); Assertions.assertThat(spy.getView()).isShown(); spy.getView().getViewTreeObserver().dispatchOnGlobalLayout(); verify(spy, times(1)).onViewWillAppear(); @@ -352,4 +353,3 @@ public void onMeasureChild() { verify(spy).applyTopInset(); } } - diff --git a/android/src/test/java/com/reactnativenavigation/views/element/TransitionAnimatorCreatorTest.kt b/android/src/test/java/com/reactnativenavigation/views/element/TransitionAnimatorCreatorTest.kt new file mode 100644 index 00000000000..525f32ec610 --- /dev/null +++ b/android/src/test/java/com/reactnativenavigation/views/element/TransitionAnimatorCreatorTest.kt @@ -0,0 +1,89 @@ +package com.reactnativenavigation.views.element + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.reactnativenavigation.options.AnimationOptions +import com.reactnativenavigation.options.LayoutAnimation +import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class TransitionAnimatorCreatorTest { + private val context = RuntimeEnvironment.getApplication() + private val parent = FrameLayout(context) + private val overlay = FrameLayout(context) + private val back = View(context) + private val stationary = View(context) + private val front = View(context) + + @Test + fun reparentsInNativeDrawingOrderAndRestoresSiblingIndicesOnEnd() { + val animator = createAnimator() + assertThat(overlay.getChildAt(0)).isSameAs(back) + assertThat(overlay.getChildAt(1)).isSameAs(front) + + animator.start() + animator.end() + assertRestored() + } + + @Test + fun restoresSiblingIndicesOnCancel() { + val animator = createAnimator() + animator.start() + animator.cancel() + assertRestored() + } + + private fun createAnimator(): AnimatorSet = runBlocking { + parent.addView(back) + parent.addView(stationary) + parent.addView(front) + val controller = mock>() + doAnswer { + overlay.addView(it.getArgument(0), it.getArgument(1)) + }.whenever(controller).addOverlay(any(), any()) + doAnswer { + overlay.removeView(it.getArgument(0)) + }.whenever(controller).removeOverlay(any()) + + fun transition(element: View) = mock().apply { + whenever(view).thenReturn(element) + whenever(viewController).thenReturn(controller) + whenever(createAnimators()).thenReturn(ValueAnimator.ofFloat(0f, 1f)) + } + + val transitions = TransitionSet().apply { + add(transition(front)) + add(transition(back)) + } + val layoutAnimation = mock() + val creator = mock() + whenever(creator.create(layoutAnimation, controller, controller)).thenReturn(transitions) + TransitionAnimatorCreator(creator).create( + layoutAnimation, AnimationOptions(), controller, controller + ) + } + + private fun assertRestored() { + assertThat(overlay.childCount).isZero() + assertThat(parent.childCount).isEqualTo(3) + assertThat(parent.getChildAt(0)).isSameAs(back) + assertThat(parent.getChildAt(1)).isSameAs(stationary) + assertThat(parent.getChildAt(2)).isSameAs(front) + } +} diff --git a/android/src/test/java/com/reactnativenavigation/views/element/ViewHierarchyComparatorTest.kt b/android/src/test/java/com/reactnativenavigation/views/element/ViewHierarchyComparatorTest.kt new file mode 100644 index 00000000000..27980597c16 --- /dev/null +++ b/android/src/test/java/com/reactnativenavigation/views/element/ViewHierarchyComparatorTest.kt @@ -0,0 +1,79 @@ +package com.reactnativenavigation.views.element + +import android.view.View +import android.widget.FrameLayout +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class ViewHierarchyComparatorTest { + private val context = RuntimeEnvironment.getApplication() + + @Test + fun preservesFabricSiblingOrderRegardlessOfTransitionDeclarationOrder() { + val parent = FrameLayout(context) + val back = View(context) + val front = View(context) + parent.addView(back) + parent.addView(front) + + assertThat(listOf(front, back).sortedWith(ViewHierarchyComparator)) + .containsExactly(back, front) + } + + @Test + fun comparesNestedViewsUsingTheirAncestorOrder() { + val root = FrameLayout(context) + val backParent = FrameLayout(context) + val frontParent = FrameLayout(context) + root.addView(backParent) + root.addView(frontParent) + backParent.addView(View(context)) + val back = View(context) + val front = View(context) + backParent.addView(back) + frontParent.addView(front) + + assertThat(listOf(front, back).sortedWith(ViewHierarchyComparator)) + .containsExactly(back, front) + } + + @Test + fun respectsAndroidElevationBeforeSiblingIndex() { + val parent = FrameLayout(context) + val elevated = View(context).apply { elevation = 4f } + val back = View(context) + parent.addView(elevated) + parent.addView(back) + + assertThat(listOf(elevated, back).sortedWith(ViewHierarchyComparator)) + .containsExactly(back, elevated) + } + + @Test + fun ordersAnAncestorBeforeItsDescendant() { + val root = FrameLayout(context) + val parent = FrameLayout(context) + val child = View(context) + root.addView(parent) + parent.addView(child) + + assertThat(listOf(child, parent).sortedWith(ViewHierarchyComparator)) + .containsExactly(parent, child) + assertThat(ViewHierarchyComparator.compare(child, child)).isZero() + } + + @Test + fun detachedViewsRetainDeclarationOrder() { + val first = View(context) + val second = View(context) + + assertThat(listOf(first, second).sortedWith(ViewHierarchyComparator)) + .containsExactly(first, second) + } +} diff --git a/autolink/fixtures/rn87/MainApplication.kt.template b/autolink/fixtures/rn87/MainApplication.kt.template new file mode 100644 index 00000000000..24d17ceb4fe --- /dev/null +++ b/autolink/fixtures/rn87/MainApplication.kt.template @@ -0,0 +1,27 @@ +package com.helloworld + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost + +class MainApplication : Application(), ReactApplication { + + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + }, + ) + } + + override fun onCreate() { + super.onCreate() + loadReactNative(this) + } +} diff --git a/autolink/postlink/__snapshots__/applicationLinker.test.js.snap b/autolink/postlink/__snapshots__/applicationLinker.test.js.snap index 282f1e5fc7c..331966856b4 100644 --- a/autolink/postlink/__snapshots__/applicationLinker.test.js.snap +++ b/autolink/postlink/__snapshots__/applicationLinker.test.js.snap @@ -19,7 +19,7 @@ import com.facebook.soloader.SoLoader class MainApplication : NavigationApplication() { override val reactNativeHost: ReactNativeHost = - object : NavigationReactNativeHost(this) { + object : NavigationReactNativeHost(this@MainApplication) { override fun getPackages(): List = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: diff --git a/autolink/postlink/applicationLinker.js b/autolink/postlink/applicationLinker.js index b77d1496487..bf07e582a5b 100644 --- a/autolink/postlink/applicationLinker.js +++ b/autolink/postlink/applicationLinker.js @@ -106,15 +106,21 @@ class ApplicationLinker { } _extendNavigationHost(applicationContent) { + // Modern templates configure ReactHost directly, without a legacy ReactNativeHost. + if (/override\s+val\s+reactHost\s*:\s*ReactHost\s+by\s+lazy/.test(applicationContent) && + !/override\s+val\s+reactNativeHost\s*:/.test(applicationContent)) { + return applicationContent; + } + if (this._hasAlreadyLinkedNavigationHost(applicationContent)) { warnn(' NavigationReactNativeHost is already used, skipping.'); - return applicationContent; + return applicationContent.replace('NavigationReactNativeHost(this)', 'NavigationReactNativeHost(this@MainApplication)'); } if (this._doesExtendDefaultReactNativeHost(applicationContent)) { debugn(' Changing host implementation to NavigationReactNativeHost'); return applicationContent - .replace('DefaultReactNativeHost(this)', 'NavigationReactNativeHost(this)') + .replace(/DefaultReactNativeHost\(this(?:@MainApplication)?\)/, 'NavigationReactNativeHost(this@MainApplication)') .replace( 'import com.facebook.react.defaults.DefaultReactNativeHost', 'import com.facebook.react.defaults.DefaultReactNativeHost\nimport com.reactnativenavigation.react.NavigationReactNativeHost' @@ -122,7 +128,7 @@ class ApplicationLinker { } else if (this._doesExtendReactNativeHost(applicationContent)) { debugn(' Changing host implementation to NavigationReactNativeHost'); return applicationContent - .replace('ReactNativeHost(this)', 'NavigationReactNativeHost(this)') + .replace(/ReactNativeHost\(this(?:@MainApplication)?\)/, 'NavigationReactNativeHost(this@MainApplication)') .replace( 'import com.facebook.react.ReactNativeHost', 'import com.facebook.react.ReactNativeHost\nimport com.reactnativenavigation.react.NavigationReactNativeHost' @@ -133,15 +139,15 @@ class ApplicationLinker { } _doesExtendReactNativeHost(applicationContent) { - return /\s*ReactNativeHost\(this\)\s*/.test(applicationContent); + return /\s*ReactNativeHost\(this(?:@MainApplication)?\)\s*/.test(applicationContent); } _doesExtendDefaultReactNativeHost(applicationContent) { - return /\s*DefaultReactNativeHost\(this\)\s*/.test(applicationContent); + return /\s*DefaultReactNativeHost\(this(?:@MainApplication)?\)\s*/.test(applicationContent); } _hasAlreadyLinkedNavigationHost(applicationContent) { - return /\s*NavigationReactNativeHost\(this\)\s*/.test(applicationContent); + return /\s*NavigationReactNativeHost\(this(?:@MainApplication)?\)\s*/.test(applicationContent); } _removeSOLoaderInit(applicationContent) { @@ -163,6 +169,13 @@ class ApplicationLinker { } _removeNewArchEntryPointLoad(applicationContent) { + // NavigationApplication.onCreate already initializes React Native. + if (/\bloadReactNative\(this\)/.test(applicationContent)) { + return applicationContent + .replace(/^import com\.facebook\.react\.ReactNativeApplicationEntryPoint\.loadReactNative\r?\n/m, '') + .replace(/^\s*loadReactNative\(this\)\s*;?\s*$/m, ''); + } + if (this._isNewArchEntryPointLoadCalled(applicationContent)) { debugn(' Removing New Architecture entry point load block'); return applicationContent.replace( diff --git a/autolink/postlink/applicationLinker.test.js b/autolink/postlink/applicationLinker.test.js index bf219d25dc4..260a33d4600 100644 --- a/autolink/postlink/applicationLinker.test.js +++ b/autolink/postlink/applicationLinker.test.js @@ -4,6 +4,27 @@ import * as mockHelpers from './__helpers__/fixtures'; jest.mock('./log'); describe('applicationLinker', () => { + it('links the RN 0.87 ReactHost template without introducing a legacy host', () => { + jest.mock('./path', () => ({})); + const ApplicationLinker = require('./applicationLinker'); + const linker = new ApplicationLinker(); + linker.applicationPath = mockHelpers.prepareFixtureDuplicate({ + rnVersion: '87', + userFixtureFileName: 'MainApplication.kt.template', + patchedFixtureFileName: 'rnn-tests_MainApplication87.kt', + }); + linker.link(); + const content = fs.readFileSync(linker.applicationPath, 'utf8'); + expect(content).toContain('class MainApplication : NavigationApplication()'); + expect(content).toContain('override val reactHost: ReactHost by lazy'); + expect(content).toContain('PackageList(this).packages.apply'); + expect(content).not.toContain('loadReactNative'); + expect(content).not.toContain('reactNativeHost'); + expect(linker.navigationHostSuccess).toBe(true); + linker.link(); + expect(fs.readFileSync(linker.applicationPath, 'utf8')).toBe(content); + }); + it('should work for RN 0.77', () => { jest.mock('./path', () => { const mainApplicationPath = mockHelpers.prepareFixtureDuplicate77({ diff --git a/ios/RNNAppDelegate.mm b/ios/RNNAppDelegate.mm index da6914a738a..b79bf012952 100644 --- a/ios/RNNAppDelegate.mm +++ b/ios/RNNAppDelegate.mm @@ -2,6 +2,7 @@ #import #import #import +#import #import "RCTAppSetupUtils.h" #if __has_include() @@ -92,9 +93,13 @@ - (BOOL)application:(UIApplication *)application self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self.reactNativeDelegate]; self.reactNativeDelegate.dependencyProvider = [RCTAppDependencyProvider new]; +#if !defined(REACT_NATIVE_VERSION_MINOR) || REACT_NATIVE_VERSION_MINOR < 87 RCTAppSetupPrepareApp(application, YES); +#endif RCTEnableTurboModuleInterop(YES); +#if !defined(REACT_NATIVE_VERSION_MINOR) || REACT_NATIVE_VERSION_MINOR < 87 RCTEnableTurboModuleInteropBridgeProxy(YES); +#endif self.reactNativeFactory.rootViewFactory.reactHost = [self.reactNativeFactory.rootViewFactory createReactHost:launchOptions]; diff --git a/ios/RNNEventEmitter.mm b/ios/RNNEventEmitter.mm index fe03a354ed9..5338e734880 100644 --- a/ios/RNNEventEmitter.mm +++ b/ios/RNNEventEmitter.mm @@ -1,7 +1,7 @@ #import "RNNEventEmitter.h" #import "RNNUtils.h" #import "RNNTurboEventEmitter.h" -#import +#import @implementation RNNEventEmitter { } diff --git a/ios/RNNReactButtonView.h b/ios/RNNReactButtonView.h index 45103d942b3..a3a5ab2210d 100644 --- a/ios/RNNReactButtonView.h +++ b/ios/RNNReactButtonView.h @@ -1,5 +1,5 @@ #import "RNNComponentView.h" -#import +#import #import #import #import diff --git a/ios/RNNReactView.mm b/ios/RNNReactView.mm index 099cb723386..bf52f524db5 100644 --- a/ios/RNNReactView.mm +++ b/ios/RNNReactView.mm @@ -1,9 +1,8 @@ #import "RNNReactView.h" #import "RNNAppDelegate.h" -#import #ifdef RCT_NEW_ARCH_ENABLED -#import +#import #import #import diff --git a/ios/RNNViewLocation.mm b/ios/RNNViewLocation.mm index 2e601239eb5..92391463240 100644 --- a/ios/RNNViewLocation.mm +++ b/ios/RNNViewLocation.mm @@ -1,6 +1,5 @@ #import "RNNViewLocation.h" #import "RNNReactView.h" -#import @implementation RNNViewLocation diff --git a/ios/ReactNativeNavigation.h b/ios/ReactNativeNavigation.h index 0c8f231486f..21ec6b1faa0 100644 --- a/ios/ReactNativeNavigation.h +++ b/ios/ReactNativeNavigation.h @@ -4,7 +4,7 @@ #import #ifdef RCT_NEW_ARCH_ENABLED -#import +#import #import "RNNTurboManager.h" #endif diff --git a/ios/ScreenAnimationController.h b/ios/ScreenAnimationController.h index 4d3c7f96715..46c6f3ff865 100644 --- a/ios/ScreenAnimationController.h +++ b/ios/ScreenAnimationController.h @@ -5,7 +5,7 @@ #import #ifdef RCT_NEW_ARCH_ENABLED -#import +#import #import #endif diff --git a/ios/TurboModules/RNNTurboManager.mm b/ios/TurboModules/RNNTurboManager.mm index 1d74ac0a098..c144f7dc7eb 100644 --- a/ios/TurboModules/RNNTurboManager.mm +++ b/ios/TurboModules/RNNTurboManager.mm @@ -9,7 +9,7 @@ #import "RNNReactComponentRegistry.h" #import "RNNReactRootViewCreator.h" #import "RNNTurboCommandsHandler.h" -#import +#import #import "RNNSplashScreenViewController.h" @interface RNNTurboManager () diff --git a/jest-setup.js b/jest-setup.js index 14630df1d2a..068818a1d1b 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -17,7 +17,7 @@ const { mockDetox } = require('detox-testing-library-rnn-adapter'); jest.mock('react-native-gesture-handler', () => { return { - gestureHandlerRootHOC: jest.fn(), + GestureHandlerRootView: require('react-native').View, }; }); diff --git a/playground/android/app/build.gradle b/playground/android/app/build.gradle index bbb88c2963b..8c7ed1082de 100644 --- a/playground/android/app/build.gradle +++ b/playground/android/app/build.gradle @@ -11,6 +11,7 @@ def jscFlavor = 'org.webkit:android-jsc:+' android { compileSdkVersion rootProject.ext.get("compileSdkVersion") + ndkVersion rootProject.ext.get("ndkVersion") namespace "com.reactnativenavigation.playground" diff --git a/playground/android/app/src/main/java/com/reactnativenavigation/playground/MainApplication.kt b/playground/android/app/src/main/java/com/reactnativenavigation/playground/MainApplication.kt index 73e9ed4a17a..28598c6ab99 100644 --- a/playground/android/app/src/main/java/com/reactnativenavigation/playground/MainApplication.kt +++ b/playground/android/app/src/main/java/com/reactnativenavigation/playground/MainApplication.kt @@ -15,7 +15,7 @@ class MainApplication : NavigationApplication(mapOf( RNNToggles.TOP_BAR_COLOR_ANIMATION__TABS to true, RNNToggles.TAB_BAR_TRANSLUCENCE to true, )) { - override val reactNativeHost: ReactNativeHost = object : NavigationReactNativeHost(this) { + override val reactNativeHost: ReactNativeHost = object : NavigationReactNativeHost(this@MainApplication) { override fun getJSMainModuleName(): String { return "index" } diff --git a/playground/android/build.gradle b/playground/android/build.gradle index a26e123a221..9af6c430f19 100644 --- a/playground/android/build.gradle +++ b/playground/android/build.gradle @@ -4,12 +4,11 @@ buildscript { apply from: './rninfo.gradle' ext { - // RN 0.79+ uses Kotlin 2.1.x, older versions use 2.0.21 - kotlinVersion = ext.rnInfo.isRN79OrHigher ? '2.1.20' : '2.0.21' + kotlinVersion = ext.rnInfo.minorVersion >= 87 ? '2.2.0' : (ext.rnInfo.isRN79OrHigher ? '2.1.20' : '2.0.21') RNNKotlinVersion = kotlinVersion detoxKotlinVersion = kotlinVersion - compileSdkVersion = 36 - buildToolsVersion = "36.0.0" + compileSdkVersion = ext.rnInfo.minorVersion >= 87 ? 37 : 36 + buildToolsVersion = ext.rnInfo.minorVersion >= 87 ? "37.0.0" : "36.0.0" minSdkVersion = 24 targetSdkVersion = 36 ndkVersion = "27.1.12297006" diff --git a/playground/android/gradle.properties b/playground/android/gradle.properties index 24f4326f621..e93ac3a8709 100644 --- a/playground/android/gradle.properties +++ b/playground/android/gradle.properties @@ -23,6 +23,9 @@ android.enableJetifier=true android.jetifier.ignorelist=bcprov hermesEnabled=true newArchEnabled=true +# Match the RN 0.87 template while third-party libraries migrate to AGP 9. +android.builtInKotlin=false +android.newDsl=false FLIPPER_VERSION=0.191.0 android.experimental.androidTest.numManagedDeviceShards=3 diff --git a/playground/e2e/DeepLinking.test.js b/playground/e2e/DeepLinking.test.js index 242760c3111..34b4f8aa221 100644 --- a/playground/e2e/DeepLinking.test.js +++ b/playground/e2e/DeepLinking.test.js @@ -1,3 +1,4 @@ +/* global device, waitFor */ import Utils from './Utils'; import TestIDs from '../src/testIDs'; @@ -28,7 +29,8 @@ describe.e2e('Deep linking', () => { it('deep-link modal can be dismissed via the close button', async () => { await device.openURL({ url: 'rnnplayground://pushed/42' }); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + // OS URL delivery completes asynchronously after Detox's openURL command. + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); await elementById(TestIDs.DEEP_LINK_CLOSE_BTN).tap(); await expect(elementById(TestIDs.NAVIGATION_SCREEN)).toBeVisible(); }); @@ -37,7 +39,7 @@ describe.e2e('Deep linking', () => { await elementById(TestIDs.SIMULATE_NESTED_DEEP_LINK_BTN).tap(); // The top-of-stack header proves the second Pushed segment mounted; // the nested-route -> multi-segment expansion is what produced it. - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('unmatched URL does not present a modal and does not crash', async () => { @@ -49,12 +51,12 @@ describe.e2e('Deep linking', () => { it('OS-delivered URL while running opens the modal', async () => { await device.openURL({ url: 'rnnplayground://pushed/77' }); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('OS-delivered URL with query params opens the modal (reserved keys filtered)', async () => { await device.openURL({ url: 'rnnplayground://pushed/77?ref=test&source=push' }); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('cold-start deep link presents the modal after root mounts', async () => { @@ -62,7 +64,7 @@ describe.e2e('Deep linking', () => { newInstance: true, url: 'rnnplayground://pushed/55', }); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('tapping a notification with a url payload opens the deep link modal', async () => { @@ -70,7 +72,7 @@ describe.e2e('Deep linking', () => { return; } await device.sendUserNotification(NOTIFICATION_PAYLOAD); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('tapping a notification with a nested url payload builds a multi-screen modal', async () => { @@ -78,7 +80,7 @@ describe.e2e('Deep linking', () => { return; } await device.sendUserNotification(NESTED_NOTIFICATION_PAYLOAD); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); it('cold-start notification tap presents the modal after root mounts', async () => { @@ -89,6 +91,6 @@ describe.e2e('Deep linking', () => { newInstance: true, userNotification: NOTIFICATION_PAYLOAD, }); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await waitFor(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible().withTimeout(5000); }); }); diff --git a/playground/e2e/Orientation.test.js b/playground/e2e/Orientation.test.js index d0154ea2142..49989813980 100644 --- a/playground/e2e/Orientation.test.js +++ b/playground/e2e/Orientation.test.js @@ -1,3 +1,4 @@ +/* global device, element, by */ import Utils from './Utils'; import TestIDs from '../src/testIDs'; @@ -6,24 +7,18 @@ const { elementById } = Utils; describe.e2e(':ios: orientation', () => { beforeEach(async () => { await device.launchApp({ newInstance: true }); - waitForDeviceToSettleAfterOrientationChangeAndroid = (ms) => - new Promise((res) => setTimeout(res, device.getPlatform() === 'ios' ? 0 : 400)); await elementById(TestIDs.NAVIGATION_TAB).tap(); await elementById(TestIDs.SHOW_ORIENTATION_SCREEN).tap(); }); - afterEach(() => { - device.setOrientation('portrait'); - }); + afterEach(() => device.setOrientation('portrait')); it('landscape and portrait array', async () => { await elementById(TestIDs.LANDSCAPE_PORTRAIT_ORIENTATION_BTN).tap(); await expect(element(by.id(TestIDs.PORTRAIT_ELEMENT))).toBeVisible(); await device.setOrientation('landscape'); - waitForDeviceToSettleAfterOrientationChangeAndroid(); await expect(element(by.id(TestIDs.LANDSCAPE_ELEMENT))).toBeVisible(); await device.setOrientation('portrait'); - waitForDeviceToSettleAfterOrientationChangeAndroid(); await expect(element(by.id(TestIDs.PORTRAIT_ELEMENT))).toBeVisible(); await elementById(TestIDs.DISMISS_BTN).tap(); }); diff --git a/playground/e2e/SharedElements.test.js b/playground/e2e/SharedElements.test.js new file mode 100644 index 00000000000..2464c1b4a58 --- /dev/null +++ b/playground/e2e/SharedElements.test.js @@ -0,0 +1,26 @@ +/* global device, waitFor, by */ +import Utils from './Utils'; +import TestIDs from '../src/testIDs'; + +const { elementById, elementByLabel } = Utils; + +describe.e2e('Shared elements', () => { + it(':android: restores nested shared elements after repeated push and pop', async () => { + await device.launchApp({ newInstance: true }); + await elementById(TestIDs.NAVIGATION_TAB).tap(); + await waitFor(elementByLabel('Shared Element (Cocktails)')) + .toBeVisible() + .whileElement(by.id(`${TestIDs.NAVIGATION_SCREEN}_LIST`)) + .scroll(200, 'down'); + await elementByLabel('Shared Element (Cocktails)').tap(); + + // The cocktail image, title and backdrop have different parents and zIndex + // values. A second transition verifies that native child order was restored. + for (let iteration = 0; iteration < 2; iteration++) { + await elementById('1').tap(); + await expect(elementById(TestIDs.COCKTAILS_DETAILS_HEADER)).toBeVisible(); + await device.pressBack(); + await expect(elementById('1')).toBeVisible(); + } + }); +}); diff --git a/playground/e2e/detox.config.js b/playground/e2e/detox.config.js index 3850d41f992..714d029b027 100644 --- a/playground/e2e/detox.config.js +++ b/playground/e2e/detox.config.js @@ -55,13 +55,13 @@ const config = { 'ios.release': { type: 'ios.app', binaryPath: 'ios/DerivedData/playground/Build/Products/Release-iphonesimulator/playground.app', - build: 'RCT_NO_LAUNCH_PACKAGER=true xcodebuild build -scheme playground_release -workspace ios/playground.xcworkspace -sdk iphonesimulator -configuration Release -derivedDataPath ios/DerivedData/playground ONLY_ACTIVE_ARCH=YES -quiet -UseModernBuildSystem=YES', + build: 'RCT_NO_LAUNCH_PACKAGER=true xcodebuild build -scheme playground -workspace ios/playground.xcworkspace -sdk iphonesimulator -configuration Release -derivedDataPath ios/DerivedData/playground ONLY_ACTIVE_ARCH=YES -quiet -UseModernBuildSystem=YES', }, 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', start: 'react-native start', - build: 'cd android && ./gradlew app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug', + build: 'cd android && ./gradlew generateCodegenArtifactsFromSchema app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug', reversePorts: [ 8081, ], @@ -69,7 +69,7 @@ const config = { 'android.release': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: 'cd android && ./gradlew app:assembleRelease app:assembleAndroidTest -DtestBuildType=release', + build: 'cd android && ./gradlew generateCodegenArtifactsFromSchema app:assembleRelease app:assembleAndroidTest -DtestBuildType=release', }, }, configurations: { @@ -121,4 +121,3 @@ const config = { }; module.exports = config; - diff --git a/playground/ios/NavigationTests/RNNCommandsHandlerTest.mm b/playground/ios/NavigationTests/RNNCommandsHandlerTest.mm index 446e292c3b8..9a76b21ccd1 100644 --- a/playground/ios/NavigationTests/RNNCommandsHandlerTest.mm +++ b/playground/ios/NavigationTests/RNNCommandsHandlerTest.mm @@ -807,8 +807,8 @@ - (void)testDismissModal_shouldResolveTopMostComponentId { RNNLayoutInfo *stackLayoutInfo = [RNNLayoutInfo new]; stackLayoutInfo.componentId = @"stack"; - RNNComponentViewController *child = [OCMockObject - partialMockForObject:[RNNComponentViewController createWithComponentId:@"child"]]; + RNNComponentViewController *child = [RNNComponentViewController createWithComponentId:@"child"]; + id childMock = OCMPartialMock(child); __unused RNNStackController *stack = [[RNNStackController alloc] initWithLayoutInfo:stackLayoutInfo creator:nil @@ -819,7 +819,7 @@ - (void)testDismissModal_shouldResolveTopMostComponentId { childViewControllers:@[ child ]]; OCMStub([self.modalManager dismissModal:OCMArg.any animated:NO completion:OCMArg.invokeBlock]); - OCMStub(child.isModal).andReturn(YES); + OCMStub([childMock isModal]).andReturn(YES); OCMStub([self.layoutManager findComponentForId:@"child"]).andReturn(child); [self.uut dismissModal:@"child" @@ -838,8 +838,8 @@ - (void)testDismissModal_shouldMergeOptions { RNNLayoutInfo *stackLayoutInfo = [RNNLayoutInfo new]; stackLayoutInfo.componentId = @"stack"; - RNNComponentViewController *child = [OCMockObject - partialMockForObject:[RNNComponentViewController createWithComponentId:@"child"]]; + RNNComponentViewController *child = [RNNComponentViewController createWithComponentId:@"child"]; + id childMock = OCMPartialMock(child); __unused RNNStackController *stack = [[RNNStackController alloc] initWithLayoutInfo:stackLayoutInfo creator:nil @@ -849,7 +849,7 @@ - (void)testDismissModal_shouldMergeOptions { eventEmitter:nil childViewControllers:@[ child ]]; - OCMStub(child.isModal).andReturn(YES); + OCMStub([childMock isModal]).andReturn(YES); OCMStub([self.layoutManager findComponentForId:@"child"]).andReturn(child); [[self.modalManager expect] diff --git a/playground/ios/NavigationTests/RNNModalManagerEventHandlerTest.mm b/playground/ios/NavigationTests/RNNModalManagerEventHandlerTest.mm index 5fb694ab7d2..b0966c0d37c 100644 --- a/playground/ios/NavigationTests/RNNModalManagerEventHandlerTest.mm +++ b/playground/ios/NavigationTests/RNNModalManagerEventHandlerTest.mm @@ -21,8 +21,7 @@ - (void)setUp { - (void)testDismissedModal_shouldEmitEventWithTopMostComponentId { RNNLayoutInfo *stackLayoutInfo = [RNNLayoutInfo new]; stackLayoutInfo.componentId = @"stack"; - RNNComponentViewController *child = [OCMockObject - partialMockForObject:[RNNComponentViewController createWithComponentId:@"child"]]; + RNNComponentViewController *child = [RNNComponentViewController createWithComponentId:@"child"]; __unused RNNStackController *stack = [[RNNStackController alloc] initWithLayoutInfo:stackLayoutInfo creator:nil @@ -40,8 +39,7 @@ - (void)testDismissedModal_shouldEmitEventWithTopMostComponentId { - (void)testAttemptToDismissModal_shouldEmitEventWithTopMostComponentId { RNNLayoutInfo *stackLayoutInfo = [RNNLayoutInfo new]; stackLayoutInfo.componentId = @"stack"; - RNNComponentViewController *child = [OCMockObject - partialMockForObject:[RNNComponentViewController createWithComponentId:@"child"]]; + RNNComponentViewController *child = [RNNComponentViewController createWithComponentId:@"child"]; __unused RNNStackController *stack = [[RNNStackController alloc] initWithLayoutInfo:stackLayoutInfo creator:nil @@ -59,8 +57,7 @@ - (void)testAttemptToDismissModal_shouldEmitEventWithTopMostComponentId { - (void)testDismissedMultipleModals_shouldEmitEventWithTopMostComponentId { RNNLayoutInfo *stackLayoutInfo = [RNNLayoutInfo new]; stackLayoutInfo.componentId = @"stack"; - RNNComponentViewController *child = [OCMockObject - partialMockForObject:[RNNComponentViewController createWithComponentId:@"child"]]; + RNNComponentViewController *child = [RNNComponentViewController createWithComponentId:@"child"]; __unused RNNStackController *stack = [[RNNStackController alloc] initWithLayoutInfo:stackLayoutInfo creator:nil diff --git a/playground/ios/NavigationTests/RNNRootViewControllerTest.mm b/playground/ios/NavigationTests/RNNRootViewControllerTest.mm index 195677a01ba..f9c601225ac 100644 --- a/playground/ios/NavigationTests/RNNRootViewControllerTest.mm +++ b/playground/ios/NavigationTests/RNNRootViewControllerTest.mm @@ -635,9 +635,11 @@ - (void)testTopBarNoBorderOff { self.options.topBar.noBorder = [[Bool alloc] initWithValue:topBarNoBorderInput]; __unused RNNStackController *nav = [self createNavigationController]; [self.uut viewWillAppear:false]; - XCTAssertTrue(CGColorEqualToColor( - self.uut.navigationController.navigationBar.standardAppearance.shadowColor.CGColor, - [UINavigationBarAppearance new].shadowColor.CGColor)); + // RNN applies the border to the screen's appearance, as in testTopBarNoBorderOn. + // UIKit can represent its default shadow with nil. + XCTAssertEqualObjects( + self.uut.navigationItem.standardAppearance.shadowColor, + [UINavigationBarAppearance new].shadowColor); } - (void)testStatusBarBlurOn { diff --git a/playground/ios/Podfile b/playground/ios/Podfile index 1d62af0704f..cb8a2204ac1 100644 --- a/playground/ios/Podfile +++ b/playground/ios/Podfile @@ -41,6 +41,15 @@ target 'playground' do pod 'HMSegmentedControl' + # The host app already links React Native and the native modules. Test bundles + # only need their headers; linking them again loads duplicate Objective-C classes. + ['NavigationTests', 'NavigationIOS12Tests', 'SnapshotTests'].each do |test_target| + target test_target do + inherit! :search_paths + pod 'OCMock' + end + end + post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( @@ -63,36 +72,3 @@ target 'playground' do end end end - -target 'NavigationTests' do - config = use_native_modules! - - use_react_native!( - :path => config[:reactNativePath], - :app_path => "#{Pod::Config.instance.installation_root}/.." - ) - - pod 'OCMock' -end - -target 'NavigationIOS12Tests' do - config = use_native_modules! - - use_react_native!( - :path => config[:reactNativePath], - :app_path => "#{Pod::Config.instance.installation_root}/.." - ) - - pod 'OCMock' -end - -target 'SnapshotTests' do - config = use_native_modules! - - use_react_native!( - :path => config[:reactNativePath], - :app_path => "#{Pod::Config.instance.installation_root}/.." - ) - - pod 'OCMock' -end diff --git a/playground/ios/playground.xcodeproj/project.pbxproj b/playground/ios/playground.xcodeproj/project.pbxproj index 4e76a4d4b3f..1998406c4b9 100644 --- a/playground/ios/playground.xcodeproj/project.pbxproj +++ b/playground/ios/playground.xcodeproj/project.pbxproj @@ -494,8 +494,6 @@ 507C80D62429111F00F765F7 /* Sources */, 507C80D72429111F00F765F7 /* Frameworks */, 507C80D82429111F00F765F7 /* Resources */, - AB86EBB5F0AF15B43F3472FD /* [CP] Copy Pods Resources */, - D85A6267FC9FE5A55766B261 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -515,8 +513,6 @@ 50996C5923AA46DD00008F89 /* Sources */, 50996C5A23AA46DD00008F89 /* Frameworks */, 50996C5B23AA46DD00008F89 /* Resources */, - EB65541BD6775B84E7C8AEDD /* [CP] Copy Pods Resources */, - 72BA3B0BE92EC7CD48134759 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -536,8 +532,6 @@ E58D2617238587F4003F36BA /* Sources */, E58D2618238587F4003F36BA /* Frameworks */, E58D2619238587F4003F36BA /* Resources */, - 8A0391DE1BA0B3DE9D40DE96 /* [CP] Copy Pods Resources */, - AB7C03026562E29128D8F500 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -760,90 +754,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 72BA3B0BE92EC7CD48134759 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NavigationIOS12Tests/Pods-NavigationIOS12Tests-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NavigationIOS12Tests/Pods-NavigationIOS12Tests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 8A0391DE1BA0B3DE9D40DE96 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NavigationTests/Pods-NavigationTests-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NavigationTests/Pods-NavigationTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - AB7C03026562E29128D8F500 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NavigationTests/Pods-NavigationTests-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NavigationTests/Pods-NavigationTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - AB86EBB5F0AF15B43F3472FD /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; D80DD80E880A67F5575078C4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -866,48 +776,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - D85A6267FC9FE5A55766B261 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - EB65541BD6775B84E7C8AEDD /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NavigationIOS12Tests/Pods-NavigationIOS12Tests-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NavigationIOS12Tests/Pods-NavigationIOS12Tests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1244,6 +1112,7 @@ "$(inherited)", "$(SRCROOT)/../../ios/**", "$(SRCROOT)/NavigationTests", + "${PODS_ROOT}/Headers/Private", ); INFOPLIST_FILE = SnapshotTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -1276,6 +1145,7 @@ "$(inherited)", "$(SRCROOT)/../../ios/**", "$(SRCROOT)/NavigationTests", + "${PODS_ROOT}/Headers/Private", ); INFOPLIST_FILE = SnapshotTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/playground/src/app.ts b/playground/src/app.ts index e8a077eac4e..3791b54c836 100644 --- a/playground/src/app.ts +++ b/playground/src/app.ts @@ -4,19 +4,6 @@ import { registerScreens } from './screens'; import addProcessors from './commons/Processors'; import { setDefaultOptions } from './commons/options/Options'; import testIDs from './testIDs'; -import Screens from './screens/Screens'; - -// @ts-ignore -alert = (title, message) => - Navigation.showOverlay({ - component: { - name: Screens.Alert, - passProps: { - title, - message, - }, - }, - }); function start() { registerScreens(); diff --git a/playground/src/components/withGestureHandlerRoot.test.tsx b/playground/src/components/withGestureHandlerRoot.test.tsx new file mode 100644 index 00000000000..de1c2c9a665 --- /dev/null +++ b/playground/src/components/withGestureHandlerRoot.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Text } from 'react-native'; +import { act, create, ReactTestRenderer } from 'react-test-renderer'; +import withGestureHandlerRoot from './withGestureHandlerRoot'; + +jest.mock('react-native-gesture-handler', () => ({ + GestureHandlerRootView: require('react-native').View, +})); + +it('forwards screen props and preserves static navigation options', () => { + function Screen({ label }: { label: string }) { + return {label}; + } + Screen.options = { topBar: { visible: false } }; + const Wrapped = withGestureHandlerRoot(Screen); + + expect(Wrapped).toHaveProperty('options', Screen.options); + let renderer!: ReactTestRenderer; + act(() => { + renderer = create(); + }); + expect(renderer.root.findByType(Text).props.children).toBe('Gesture screen'); + act(() => renderer.unmount()); +}); diff --git a/playground/src/components/withGestureHandlerRoot.tsx b/playground/src/components/withGestureHandlerRoot.tsx new file mode 100644 index 00000000000..daf735e1590 --- /dev/null +++ b/playground/src/components/withGestureHandlerRoot.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import hoistNonReactStatics from 'hoist-non-react-statics'; + +const rootStyle = { flex: 1 }; + +// Gesture Handler 3 removes gestureHandlerRootHOC. Keep the same root layout +// and screen statics (including RNN options) on both Gesture Handler 2 and 3. +export default function withGestureHandlerRoot

(Screen: React.ComponentType

) { + function GestureRoot(props: P) { + return ( + + + + ); + } + return hoistNonReactStatics(GestureRoot, Screen); +} diff --git a/playground/src/screens/BottomTabsStylingScreen.tsx b/playground/src/screens/BottomTabsStylingScreen.tsx index cd06fd33d97..5d553a2968a 100644 --- a/playground/src/screens/BottomTabsStylingScreen.tsx +++ b/playground/src/screens/BottomTabsStylingScreen.tsx @@ -2,11 +2,8 @@ import React, { Component } from 'react'; import { NavigationProps, Options } from 'react-native-navigation'; import { Text, StyleSheet, View, ScrollView, Platform } from 'react-native'; -import Root from '../components/Root'; import Button from '../components/Button'; import Navigation from '../services/Navigation'; -import Screens from './Screens'; -import { stack } from '../commons/Layouts'; const isIOS26 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 26; diff --git a/playground/src/screens/ButtonsScreen.tsx b/playground/src/screens/ButtonsScreen.tsx index d00d8434dd0..edc6bf72512 100644 --- a/playground/src/screens/ButtonsScreen.tsx +++ b/playground/src/screens/ButtonsScreen.tsx @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import React from 'react'; import { Platform } from 'react-native'; import { NavigationComponent, Options, OptionsTopBarButton } from 'react-native-navigation'; diff --git a/playground/src/screens/FirstBottomTabScreen.tsx b/playground/src/screens/FirstBottomTabScreen.tsx index c8d1164c543..0be5fda401d 100644 --- a/playground/src/screens/FirstBottomTabScreen.tsx +++ b/playground/src/screens/FirstBottomTabScreen.tsx @@ -1,3 +1,4 @@ +import showAlert from '../services/Alert'; import React, { Component } from 'react'; import { EmitterSubscription, Platform, Text } from 'react-native'; import { NavigationProps, Options } from 'react-native-navigation'; @@ -80,7 +81,7 @@ export default class FirstBottomTabScreen extends Component { return Navigation.events().registerBottomTabPressedListener((event) => { if (event.tabIndex == 2) { - alert('BottomTabPressed'); + showAlert('BottomTabPressed'); } }); }; diff --git a/playground/src/screens/FlatListScreen.tsx b/playground/src/screens/FlatListScreen.tsx index 06ee41802cb..d78ab148449 100644 --- a/playground/src/screens/FlatListScreen.tsx +++ b/playground/src/screens/FlatListScreen.tsx @@ -1,3 +1,4 @@ +import showAlert from '../services/Alert'; import React from 'react'; import { FlatList, View, Text } from 'react-native'; import { @@ -92,7 +93,7 @@ export default class FlatListScreen extends NavigationComponent { } componentDidDisappear() { - this.showUnmountAndDisappearAlerts && alert('didDisappear'); // eslint-disable-line no-alert + this.showUnmountAndDisappearAlerts && showAlert('didDisappear'); } componentWillUnmount() { setTimeout(() => { - this.showUnmountAndDisappearAlerts && alert('componentWillUnmount'); // eslint-disable-line no-alert + this.showUnmountAndDisappearAlerts && showAlert('componentWillUnmount'); }, 100); } navigationButtonPressed(id: NavigationButtonPressedEvent) { - alert(`navigationButtonPressed: ${id}`); // eslint-disable-line no-alert + showAlert(`navigationButtonPressed: ${id}`); } render() { @@ -87,7 +88,7 @@ export default class LifecycleScreen extends React.Component { this.showUnmountAndDisappearAlerts = false; const promise = new Promise((resolve) => { const unregister = Navigation.events().registerScreenPoppedListener(() => { - alert('Screen popped event'); + showAlert('Screen popped event'); unregister.remove(); resolve(); }); diff --git a/playground/src/screens/OverlayScreen.tsx b/playground/src/screens/OverlayScreen.tsx index fb8d6f28752..4905cbf759a 100644 --- a/playground/src/screens/OverlayScreen.tsx +++ b/playground/src/screens/OverlayScreen.tsx @@ -1,3 +1,4 @@ +import showAlert from '../services/Alert'; import React from 'react'; import { NavigationProps } from 'react-native-navigation'; import Root from '../components/Root'; @@ -64,7 +65,7 @@ export default class OverlayScreen extends React.Component { {this.state?.overlayDismissedCount || ''}