* iOS

* Android

* update

* RN -0.84 android

* update for android

* test update for hardcoded height - from 412 to 394

* revert test update

* all tests were running on latest RN instead of the right onw

* yarn change

* pod install update to run only once

* patch for codegen for Android webview

* some minor changes in regex and iOS install script

* regex update

* test update for android

* another test update for android

* very flaky test removed

* removal of rules

* reverted the removed test

* API 36 fix for button test

* safety added to BridgeIdlingResource.kt
This commit is contained in:
mark.dev
2026-04-19 09:29:53 +03:00
committed by GitHub
parent 428e458935
commit b8e1c7ebb1
36 changed files with 417 additions and 186 deletions
@@ -5,7 +5,6 @@
env:
REACT_NATIVE_VERSION: 0.83.0
RCT_NEW_ARCH_ENABLED: 1
REACT_NATIVE_COMPAT_TEST: true # Only set 'true' in jobs with the latest supported RN
DETOX_DISABLE_POD_INSTALL: true
DETOX_DISABLE_POSTINSTALL: true
artifact_paths:
@@ -0,0 +1,13 @@
- label: ":android::react: RN .84 + Android: Demo app"
command:
- "nvm install"
- "./scripts/demo-projects.android.sh"
env:
REACT_NATIVE_VERSION: 0.84.0
RCT_NEW_ARCH_ENABLED: 1
REACT_NATIVE_COMPAT_TEST: true
DETOX_DISABLE_POD_INSTALL: true
DETOX_DISABLE_POSTINSTALL: true
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
@@ -0,0 +1,13 @@
- label: ":android::detox: RN .84 + Android: Tests app"
command:
- "nvm install"
- "./scripts/ci.android.sh"
env:
REACT_NATIVE_VERSION: 0.84.0
RCT_NEW_ARCH_ENABLED: 1
DETOX_DISABLE_POD_INSTALL: true
DETOX_DISABLE_POSTINSTALL: true
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
@@ -5,7 +5,6 @@
env:
REACT_NATIVE_VERSION: 0.83.0
RCT_NEW_ARCH_ENABLED: 1
REACT_NATIVE_COMPAT_TEST: true # Only set 'true' in jobs with the latest supported RN
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
@@ -0,0 +1,11 @@
- label: ":ios::react: RN .84 + iOS: Demo app"
command:
- "nvm install"
- "./scripts/demo-projects.ios.sh"
env:
REACT_NATIVE_VERSION: 0.84.0
RCT_NEW_ARCH_ENABLED: 1
REACT_NATIVE_COMPAT_TEST: true
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
+11
View File
@@ -0,0 +1,11 @@
- label: ":ios::detox: RN .84 + iOS: Tests app"
command:
- "nvm install"
- "./scripts/ci.ios.sh"
env:
REACT_NATIVE_VERSION: 0.84.0
RCT_NEW_ARCH_ENABLED: 1
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
+4 -2
View File
@@ -2,10 +2,12 @@
echo "steps:"
cat .buildkite/jobs/pipeline.android_demo_app_rn_83.yml
cat .buildkite/jobs/pipeline.android_demo_app_rn_84.yml
cat .buildkite/jobs/pipeline.android_rn_77.yml
cat .buildkite/jobs/pipeline.android_rn_83.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_83.yml
cat .buildkite/jobs/pipeline.android_rn_84.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_84.yml
cat .buildkite/jobs/pipeline.ios_rn_77.yml
cat .buildkite/jobs/pipeline.ios_rn_83.yml
cat .buildkite/jobs/pipeline.ios_rn_84.yml
cat .buildkite/pipeline.post_processing.yml
-8
View File
@@ -1,8 +0,0 @@
---
description: Rules to help Cursor write code that is more Detox idomatic
globs: *.js
alwaysApply: false
---
- Whenever an Error needs to be thrown, prefer throwing a DetoxRuntimeError with a proper message and hint.
- Minimize comments usage, especially in-line ones. Leave comments where things are exceptionally unclear for some reason.
-62
View File
@@ -1,62 +0,0 @@
---
Description: Rules to help Cursor write unit tests code that aligns with the project's standards
globs: *.test.js,*.test.ts
alwaysApply: false
---
# Project Rules
## Jest Unit Testing
In Jest unit test files, **never perform mocking globally**. Instead, perform all mocks inside the `beforeEach()` section.
### Rationale
- Keeps test setup centralized and visible
- Ensures mocks are reset between tests
- Makes test dependencies explicit
- Prevents test pollution from global mocks
### Example
❌ **Don't do this:**
```javascript
const mockSleep = jest.fn().mockResolvedValue(undefined);
jest.mock('../../../../utils/sleep', () => mockSleep);
describe('MyClass', () => {
// ...
});
```
✅ **Do this instead:**
```javascript
describe('MyClass', () => {
let mockSleep;
beforeEach(() => {
jest.mock('../../../../utils/sleep');
mockSleep = require('../../../../utils/sleep');
mockSleep.mockResolvedValue(undefined);
// ... other setup
});
});
```
Alternatively, in cases where jest.mock() doesn't work well out of the box, this can be performed too:
```javascript
beforeEach(() => {
jest.mock('../../../../utils/sleep', () => jest.fn().mockResolvedValue(undefined));
mockSleep = require('../../../../utils/sleep');
});
// Or if the sleep js module actually exports an object:
beforeEach(() => {
jest.mock('../../../../utils/sleep', () => ({
sleep: jest.fn().mockResolvedValue(undefined),
}));
mockSleep = require('../../../../utils/sleep');
});
```
Note: `jest.mock()` must still be called at the module level (for hoisting), but all mock configuration should happen in `beforeEach()`.
+1
View File
@@ -49,6 +49,7 @@ jspm_packages
################
.idea
.vscode
.cursor
## File-based project format:
*.iws
+1 -1
View File
@@ -63,7 +63,7 @@ Detox was built from the ground up to support React Native projects.
While Detox should work out of the box with any React Native version of the latest minor releases,
the _official_ versions compatibility is provided according to the following:
- **RN `v0.77.x` - `v0.83.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
- **RN `v0.77.x` - `v0.84.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
Newer RN versions might work with Detox, but they've not been thoroughly tested by the Detox team yet.
Although we do not officially support older React Native versions, we do our best to keep Detox compatible with them.
@@ -14,7 +14,7 @@ fun ReactApplication.getInstanceManagerSafe(): ReactInstanceManager {
@SuppressLint("VisibleForTests")
fun ReactApplication.getCurrentReactContext(): ReactContext? {
return if (isFabricEnabled()) {
return if (isFabricEnabled() || ReactNativeInfo.isNewArchitectureOnlyVersion()) {
reactHost?.currentReactContext
} else {
getInstanceManagerSafe().currentReactContext
@@ -2,10 +2,16 @@ package com.wix.detox.reactnative
import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.test.platform.app.InstrumentationRegistry
import com.facebook.react.ReactApplication
import com.facebook.react.bridge.ReactContext
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.wix.detox.LaunchArgs
import com.wix.detox.reactnative.idlingresources.ReactNativeIdlingResources
import com.wix.detox.reactnative.reloader.ReactNativeReloaderFactory
@@ -14,6 +20,7 @@ private const val LOG_TAG = "DetoxRNExt"
object ReactNativeExtension {
private var rnIdlingResources: ReactNativeIdlingResources? = null
private var backPressCallbackRegistered = false
fun initIfNeeded() {
if (!ReactNativeInfo.isReactNativeApp()) {
@@ -37,6 +44,7 @@ object ReactNativeExtension {
awaitNewReactNativeContext(it, null)
enableOrDisableSynchronization(it)
registerBackPressCallbackIfNeeded(applicationContext, it)
}
}
@@ -58,6 +66,7 @@ object ReactNativeExtension {
(applicationContext as ReactApplication).let {
clearIdlingResources()
backPressCallbackRegistered = false
val previousReactContext = it.getCurrentReactContext()
@@ -65,6 +74,7 @@ object ReactNativeExtension {
awaitNewReactNativeContext(it, previousReactContext)
enableOrDisableSynchronization(it)
registerBackPressCallbackIfNeeded(applicationContext, it)
}
}
@@ -110,6 +120,43 @@ object ReactNativeExtension {
}
}
/**
* On API 35+, the system routes back presses through [OnBackPressedDispatcher] instead of
* calling [Activity.onBackPressed]. Older RN versions (< 0.83) don't register an
* [OnBackPressedCallback], so the back press bypasses React Native's BackHandler entirely,
* causing the activity to finish instead of letting JS handle it.
*
* This registers a callback that routes back presses through [ReactInstanceManager.onBackPressed],
* restoring the expected behavior.
*/
private fun registerBackPressCallbackIfNeeded(context: Context, reactApp: ReactApplication) {
if (backPressCallbackRegistered) return
if (Build.VERSION.SDK_INT < 35) return
if (ReactNativeInfo.isNewArchitectureOnlyVersion()) return
val activity = getRNActivity(context) as? ComponentActivity ?: return
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
try {
val reactContext = reactApp.getCurrentReactContext()
reactContext
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit("hardwareBackPress", null)
?: Log.w(LOG_TAG, "ReactContext is null, cannot emit hardwareBackPress")
} catch (e: Exception) {
Log.w(LOG_TAG, "Failed to emit hardwareBackPress", e)
}
}
}
backPressCallbackRegistered = true
Handler(Looper.getMainLooper()).post {
activity.onBackPressedDispatcher.addCallback(activity, callback)
Log.i(LOG_TAG, "Registered OnBackPressedCallback for legacy RN back press handling")
}
}
private fun reloadReactNativeInBackground(reactApplication: ReactApplication) {
val rnReloader = ReactNativeReloaderFactory(InstrumentationRegistry.getInstrumentation(), reactApplication).create()
rnReloader.reloadInBackground()
@@ -12,6 +12,13 @@ object ReactNativeInfo {
@JvmStatic
fun rnVersion() = rnVersion
@JvmStatic
fun isNewArchitectureOnlyVersion(): Boolean {
return rnVersion.run {
major > 0 || (major == 0 && minor >= 84)
}
}
fun isReactNativeApp(): Boolean = try {
Class.forName("com.facebook.react.ReactApplication")
true
@@ -83,8 +83,8 @@ open class ReactNativeLoadingMonitor(
reactContext != null && reactContext !== previousReactContext && reactContext.hasActiveReactInstance()
private fun subscribeAsyncRNContextHandler(onReactContextInitialized: () -> Any) {
val isFabric = isFabricEnabled()
if (isFabric) {
val useNewArchitectureListener = isFabricEnabled() || ReactNativeInfo.isNewArchitectureOnlyVersion()
if (useNewArchitectureListener) {
// We do a casting for supporting RN 0.75 and above
val host = rnApplication.reactHost as ReactHostImpl?
host?.addReactInstanceEventListener(object : ReactInstanceEventListener {
@@ -1,9 +1,11 @@
package com.wix.detox.reactnative.idlingresources.bridge
import android.util.Log
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener
import com.facebook.react.bridge.ReactContext
import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.atomic.AtomicBoolean
/**
@@ -16,16 +18,25 @@ import java.util.concurrent.atomic.AtomicBoolean
* React Native's JS bridge.
*
*/
class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource(),
NotThreadSafeBridgeIdleDebugListener {
class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdlingResource() {
private val idleNow = AtomicBoolean(true)
private val bridgeIdleListenerApi = BridgeIdleListenerApi(reactContext)
init {
reactContext.catalystInstance.addBridgeIdleDebugListener(this)
bridgeIdleListenerApi.attach(
onBridgeIdle = {
idleNow.set(true)
notifyIdle()
},
onBridgeBusy = {
idleNow.set(false)
},
onBridgeDestroyed = {}
)
}
fun onDetach() {
reactContext.catalystInstance.removeBridgeIdleDebugListener(this)
bridgeIdleListenerApi.detach()
}
override fun getName(): String {
@@ -48,19 +59,6 @@ class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdling
return ret
}
override fun onTransitionToBridgeIdle() {
idleNow.set(true)
notifyIdle()
}
override fun onTransitionToBridgeBusy() {
idleNow.set(false)
// Log.i(LOG_TAG, "JS Bridge transitions to busy.");
}
override fun onBridgeDestroyed() {
}
override fun onUnregistered() {
super.onUnregistered()
onDetach()
@@ -70,3 +68,72 @@ class BridgeIdlingResource(private val reactContext: ReactContext) : DetoxIdling
private const val LOG_TAG = "Detox"
}
}
private class BridgeIdleListenerApi(private val reactContext: ReactContext) {
private var listenerProxy: Any? = null
private var removeListenerMethod: Method? = null
fun attach(
onBridgeIdle: () -> Unit,
onBridgeBusy: () -> Unit,
onBridgeDestroyed: () -> Unit,
) {
try {
val listenerClass = Class.forName(BRIDGE_IDLE_LISTENER_CLASS_NAME)
val catalystInstance = reactContext.catalystInstance
val addListenerMethod = catalystInstance.javaClass.methods.firstOrNull { method ->
method.name == ADD_LISTENER_METHOD && method.parameterTypes.contentEquals(arrayOf(listenerClass))
} ?: run {
Log.i(LOG_TAG, "RN bridge idle debug listener API is unavailable.")
return
}
val proxy = Proxy.newProxyInstance(
listenerClass.classLoader,
arrayOf(listenerClass),
InvocationHandler { proxy, method, args ->
when (method.name) {
ON_BRIDGE_IDLE_METHOD -> onBridgeIdle()
ON_BRIDGE_BUSY_METHOD -> onBridgeBusy()
ON_BRIDGE_DESTROYED_METHOD -> onBridgeDestroyed()
"equals" -> proxy === args?.firstOrNull()
"hashCode" -> System.identityHashCode(proxy)
"toString" -> "BridgeIdleListenerProxy"
}
}
)
addListenerMethod.invoke(catalystInstance, proxy)
listenerProxy = proxy
removeListenerMethod = catalystInstance.javaClass.methods.firstOrNull { method ->
method.name == REMOVE_LISTENER_METHOD && method.parameterTypes.contentEquals(arrayOf(listenerClass))
}
} catch (e: Throwable) {
Log.w(LOG_TAG, "Could not attach RN bridge idle debug listener.", e)
}
}
fun detach() {
val listener = listenerProxy ?: return
val removeMethod = removeListenerMethod ?: return
try {
removeMethod.invoke(reactContext.catalystInstance, listener)
} catch (e: Throwable) {
Log.w(LOG_TAG, "Could not detach RN bridge idle debug listener.", e)
} finally {
listenerProxy = null
removeListenerMethod = null
}
}
companion object {
private const val LOG_TAG = "Detox"
private const val BRIDGE_IDLE_LISTENER_CLASS_NAME =
"com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener"
private const val ADD_LISTENER_METHOD = "addBridgeIdleDebugListener"
private const val REMOVE_LISTENER_METHOD = "removeBridgeIdleDebugListener"
private const val ON_BRIDGE_IDLE_METHOD = "onTransitionToBridgeIdle"
private const val ON_BRIDGE_BUSY_METHOD = "onTransitionToBridgeBusy"
private const val ON_BRIDGE_DESTROYED_METHOD = "onBridgeDestroyed"
}
}
@@ -1,6 +1,7 @@
package com.wix.detox.reactnative.idlingresources.factory
import com.facebook.react.ReactApplication
import com.wix.detox.reactnative.ReactNativeInfo
import com.wix.detox.reactnative.idlingresources.DetoxIdlingResource
import com.wix.detox.reactnative.isFabricEnabled
import kotlinx.coroutines.Dispatchers
@@ -9,7 +10,8 @@ import kotlinx.coroutines.withContext
class DetoxIdlingResourceFactory(private val reactApplication: ReactApplication) {
suspend fun create(): Map<IdlingResourcesName, DetoxIdlingResource> = withContext(Dispatchers.Main) {
val strategy = if (isFabricEnabled()) {
val useNewArchitectureStrategy = isFabricEnabled() || ReactNativeInfo.isNewArchitectureOnlyVersion()
val strategy = if (useNewArchitectureStrategy) {
FabricDetoxIdlingResourceFactoryStrategy(reactApplication)
} else {
OldArchitectureDetoxIdlingResourceFactoryStrategy(reactApplication)
@@ -1,5 +1,6 @@
package com.wix.detox.reactnative.idlingresources.uimodule.fabric
import android.os.SystemClock
import android.view.Choreographer
import androidx.test.espresso.IdlingResource
import com.facebook.react.bridge.ReactContext
@@ -15,14 +16,51 @@ class FabricUIManagerIdlingResources(
private val reactContext: ReactContext
) : DetoxIdlingResource(), Choreographer.FrameCallback {
private var firstBusyTimestamp: Long = 0
private var isSteadyState: Boolean = false
override fun checkIdle(): Boolean {
return if (getViewCommandMountItemsSize() == 0 && getMountItemsSize() == 0) {
val mountItemsCount = getMountItemsSize()
val viewCommandMountItemsCount = getViewCommandMountItemsSize()
if (mountItemsCount == 0 && viewCommandMountItemsCount == 0) {
firstBusyTimestamp = 0
isSteadyState = false
notifyIdle()
true
} else {
Choreographer.getInstance().postFrameCallback(this)
false
return true
}
// Once we've determined this is a steady-state (a stuck mount item that never
// resolves), keep reporting idle as long as the count stays low.
if (isSteadyState && mountItemsCount <= 1 && viewCommandMountItemsCount == 0) {
notifyIdle()
return true
}
// Count increased beyond steady-state threshold — reset and treat as genuinely busy.
if (isSteadyState) {
isSteadyState = false
firstBusyTimestamp = 0
}
val now = SystemClock.uptimeMillis()
if (firstBusyTimestamp == 0L) {
firstBusyTimestamp = now
}
// On API 36+, edge-to-edge enforcement can cause a single mount item to remain
// permanently in the queue on older RN versions. If the count stays at 1 for over
// 1.5s, treat it as a steady-state condition rather than a genuinely busy UI.
if (now - firstBusyTimestamp >= BUSY_TOLERANCE_MS
&& mountItemsCount <= 1
&& viewCommandMountItemsCount == 0) {
isSteadyState = true
notifyIdle()
return true
}
Choreographer.getInstance().postFrameCallback(this)
return false
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
@@ -78,4 +116,7 @@ class FabricUIManagerIdlingResources(
return viewCommandMountItems.size
}
companion object {
private const val BUSY_TOLERANCE_MS = 1500L
}
}
@@ -2,6 +2,7 @@ package com.wix.detox.reactnative.reloader
import android.app.Instrumentation
import com.facebook.react.ReactApplication
import com.wix.detox.reactnative.ReactNativeInfo
import com.wix.detox.reactnative.isFabricEnabled
class ReactNativeReloaderFactory(
@@ -11,7 +12,8 @@ class ReactNativeReloaderFactory(
fun create(): ReactNativeReLoader {
return when {
isFabricEnabled() -> NewArchitectureNativeReLoader(instrumentation, rnApplication)
isFabricEnabled() || ReactNativeInfo.isNewArchitectureOnlyVersion() ->
NewArchitectureNativeReLoader(instrumentation, rnApplication)
else -> OldArchReactNativeReLoader(instrumentation, rnApplication)
}
}
+1
View File
@@ -48,4 +48,5 @@ ext.rnInfo = [
isRN79OrHigher: rnMajorVer >= 79,
isRN80OrHigher: rnMajorVer >= 80,
isRN81OrHigher: rnMajorVer >= 81,
isRN84OrHigher: rnMajorVer >= 84,
]
+4
View File
@@ -275,6 +275,10 @@ class Element : NSObject {
@objc
var value: String? {
if let slider = view.dtx_sliderView {
return slider.accessibilityValue
}
return view.accessibilityValue
}
+1 -1
View File
@@ -82,7 +82,7 @@
"jest-allure2-reporter": "^2.2.6",
"metro-react-native-babel-preset": "0.76.8",
"prettier": "^3.1.1",
"react-native": "0.83.0",
"react-native": "0.84.0",
"react-native-codegen": "^0.0.8",
"typescript": "^5.8.3",
"wtfnode": "^0.9.1"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 81 KiB

@@ -701,7 +701,10 @@
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
@@ -709,6 +712,7 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
@@ -717,6 +721,7 @@
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
@@ -774,7 +779,10 @@
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
@@ -782,6 +790,7 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
@@ -789,6 +798,7 @@
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
+2 -2
View File
@@ -40,8 +40,8 @@
"@react-native-picker/picker": "^2.11.2",
"@react-native-segmented-control/segmented-control": "^2.5.6",
"moment": "^2.30.1",
"react": "19.2.0",
"react-native": "0.83.0",
"react": "19.2.3",
"react-native": "0.84.0",
"react-native-launch-arguments": "^4.0.2",
"react-native-permissions": "^5.2.1",
"react-native-webview": "^13.13.1",
@@ -0,0 +1,34 @@
const fs = require('fs');
const path = require('path');
const log = message => console.log(`[PATCH-WEBVIEW] ${message}`);
const testAppRoot = path.resolve(__dirname, '..');
const reactNativePackageJsonPath = path.join(testAppRoot, 'node_modules/react-native/package.json');
const webViewSpecPath = path.join(testAppRoot, 'node_modules/react-native-webview/src/RNCWebViewNativeComponent.ts');
if (!fs.existsSync(reactNativePackageJsonPath) || !fs.existsSync(webViewSpecPath)) {
log('react-native or react-native-webview is missing, skipping.');
process.exit(0);
}
const reactNativeVersion = JSON.parse(fs.readFileSync(reactNativePackageJsonPath, 'utf8')).version;
const [major, minor] = reactNativeVersion.split('.').map(Number);
if (major !== 0 || minor < 84) {
log(`react-native@${reactNativeVersion} does not need the WebView patch, skipping.`);
process.exit(0);
}
let webViewSpec = fs.readFileSync(webViewSpecPath, 'utf8');
const navigationTypeUnion = /navigationType:\s*\|?\s*'click'\s*\|\s*'formsubmit'\s*\|\s*'backforward'\s*\|\s*'reload'\s*\|\s*'formresubmit'\s*\|\s*'other';/g;
if (!navigationTypeUnion.test(webViewSpec)) {
log('Spec already patched or changed, skipping.');
process.exit(0);
}
navigationTypeUnion.lastIndex = 0;
log('Patching react-native-webview navigationType union → string for Android codegen compatibility...');
webViewSpec = webViewSpec.replace(navigationTypeUnion, 'navigationType: string;');
fs.writeFileSync(webViewSpecPath, webViewSpec, 'utf8');
log('Done.');
@@ -7,7 +7,7 @@ Detox was built from the ground up to support "classic" React Native projects.
While Detox should work out of the box with any React Native version of the latest minor releases,
the _official_ versions compatibility is provided according to the following:
- **RN `v0.77.x` - `v0.82.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
- **RN `v0.77.x` - `v0.84.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
Newer RN versions might work with Detox, but they've not been thoroughly tested by the Detox team yet.
Although we do not officially support older React Native versions, we do our best to keep Detox compatible with them.
@@ -228,10 +228,14 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${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}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -246,21 +250,15 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-example/Pods-example-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${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}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -409,12 +407,19 @@
LDPLUSPLUS = "";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
@@ -462,11 +467,18 @@
LD = "";
LDPLUSPLUS = "";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
+2 -2
View File
@@ -23,8 +23,8 @@
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.0",
"react": "19.2.0",
"react-native": "0.83.0",
"react": "19.2.3",
"react-native": "0.84.0",
"tslib": "^2.0.3"
},
"devDependencies": {
+1 -2
View File
@@ -5,6 +5,5 @@ if [ ! -z ${REACT_NATIVE_VERSION} ]; then
node scripts/change_react_native_version.js "detox/test" ${REACT_NATIVE_VERSION} "dependencies"
node scripts/change_react_native_version.js "detox" ${REACT_NATIVE_VERSION} "devDependencies"
# Update lockfile for the new RN version
yarn install --mode update-lockfile
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install
fi
+1
View File
@@ -23,6 +23,7 @@ mkdir -p coverage
### E2E self-tests
pushd detox/test
run_f "node scripts/patch-webview-codegen.js"
run_f "yarn build:android-debug" # Workaround for asyncstorage issue https://github.com/react-native-async-storage/async-storage/issues/1216. Can be removed after fixing it
run_f "yarn build:android"
@@ -7,7 +7,7 @@ Detox was built from the ground up to support "classic" React Native projects.
While Detox should work out of the box with any React Native version of the latest minor releases,
the _official_ versions compatibility is provided according to the following:
- **RN `v0.77.x` - `v0.82.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
- **RN `v0.77.x` - `v0.84.x`:** Fully compatible with React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page).
Newer RN versions might work with Detox, but they've not been thoroughly tested by the Detox team yet.
Although we do not officially support older React Native versions, we do our best to keep Detox compatible with them.
+87 -62
View File
@@ -3966,10 +3966,10 @@ __metadata:
languageName: node
linkType: hard
"@react-native/assets-registry@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/assets-registry@npm:0.83.0"
checksum: 10/9c314a311739e5ea555155df54821f888f3c61de6bcd71db60a253b27aefb42116892d487efb57a5ba99998c9835e1b123019bfe4397c087d0bcef38bad43c8c
"@react-native/assets-registry@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/assets-registry@npm:0.84.0"
checksum: 10/9f972a0e46494aafddbed140f3256615dd2d2675055b8a3dbe05ca0c30d2d89a7a960fbe6580e327a850d870203b8e89247adfd3ed629c0b54d905c6fd5dbcb3
languageName: node
linkType: hard
@@ -4055,11 +4055,28 @@ __metadata:
languageName: node
linkType: hard
"@react-native/community-cli-plugin@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/community-cli-plugin@npm:0.83.0"
"@react-native/codegen@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/codegen@npm:0.84.0"
dependencies:
"@react-native/dev-middleware": "npm:0.83.0"
"@babel/core": "npm:^7.25.2"
"@babel/parser": "npm:^7.25.3"
hermes-parser: "npm:0.32.0"
invariant: "npm:^2.2.4"
nullthrows: "npm:^1.1.1"
tinyglobby: "npm:^0.2.15"
yargs: "npm:^17.6.2"
peerDependencies:
"@babel/core": "*"
checksum: 10/1b81de2bcccbf570efb42cf8aaa4a551ececdc510ce9c9c2230f8eafdd0eeec25729307340993a769eca94c99e343499ba0aeec4427e3e7976914c01055732f7
languageName: node
linkType: hard
"@react-native/community-cli-plugin@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/community-cli-plugin@npm:0.84.0"
dependencies:
"@react-native/dev-middleware": "npm:0.84.0"
debug: "npm:^4.4.0"
invariant: "npm:^2.2.4"
metro: "npm:^0.83.3"
@@ -4074,34 +4091,35 @@ __metadata:
optional: true
"@react-native/metro-config":
optional: true
checksum: 10/b0a664d828270c7be320c84b6f6f37dc08682d899157865cd8655f5bac99d2b990c9cc4dcaf57f4c284c698af41dd97bc4acd34fec5fc052f535c83a74cf0cb9
checksum: 10/3d1d0fa6e5d904b9d3fc55c417e2f561daa8ace734ba0e4fd4cd6abb73967fe0756ec34f7a04b2f98b21eb834cffba869a79e26c57612c66c67054f6995d44b2
languageName: node
linkType: hard
"@react-native/debugger-frontend@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/debugger-frontend@npm:0.83.0"
checksum: 10/3f52e5400c24173f7619d35be95b6899f15e087e218d8ad580a5a931e908c2d0ef0e752619a73752bc5ed18f35a96fd38723c1967fa65f68e9f8cc6bd43972dc
"@react-native/debugger-frontend@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/debugger-frontend@npm:0.84.0"
checksum: 10/93435f6bb8cc69195846a25453639eeaa6b261113fae8957e3c7f28c0302e25ea89fc757bf7bb971e2e5caa0672b228fbaddbf5ab2c844d2a6365522e3fa7edd
languageName: node
linkType: hard
"@react-native/debugger-shell@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/debugger-shell@npm:0.83.0"
"@react-native/debugger-shell@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/debugger-shell@npm:0.84.0"
dependencies:
cross-spawn: "npm:^7.0.6"
debug: "npm:^4.4.0"
fb-dotslash: "npm:0.5.8"
checksum: 10/51b61131b90a9e09f259a5999759b77d9384cf9b0f38e4c4da1b63a1a729126349f9d20f51d599e527ebe5430945e2e1ddde17d933aa30c0775e2437694db50f
checksum: 10/5e29a717ce5384735f9db32fb3f073ec1bf53d7fd136780433befa04f0d31275ef5aaebc91e1e923518add0e77a70cfbcc9dc374b01a8bcf71944747d2f7bbde
languageName: node
linkType: hard
"@react-native/dev-middleware@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/dev-middleware@npm:0.83.0"
"@react-native/dev-middleware@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/dev-middleware@npm:0.84.0"
dependencies:
"@isaacs/ttlcache": "npm:^1.4.1"
"@react-native/debugger-frontend": "npm:0.83.0"
"@react-native/debugger-shell": "npm:0.83.0"
"@react-native/debugger-frontend": "npm:0.84.0"
"@react-native/debugger-shell": "npm:0.84.0"
chrome-launcher: "npm:^0.15.2"
chromium-edge-launcher: "npm:^0.2.0"
connect: "npm:^3.6.5"
@@ -4111,7 +4129,7 @@ __metadata:
open: "npm:^7.0.3"
serve-static: "npm:^1.16.2"
ws: "npm:^7.5.10"
checksum: 10/524df13d559e4593887b6183cedafb43ea0d6b07e4b04b527549efcce03e2809044937c7fc0c6483fc14fb237d74b0c706c2fd01825e3abb5a3cc009a27aba8b
checksum: 10/ac061225f248821e2d90f201423e51b23431444ef76ea5bd5cc7a5467cee40fa6c780a1d80a0bb926cb9c25d79811eb1e4de663e42202fd3776a6ea43cda8ea4
languageName: node
linkType: hard
@@ -4145,10 +4163,10 @@ __metadata:
languageName: node
linkType: hard
"@react-native/gradle-plugin@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/gradle-plugin@npm:0.83.0"
checksum: 10/dcd7a47b21629a78a6f079a16a87d85ed78214dd19efafb054e7718624774b07e8c10572d054b9e31ac80c96148127a40da164b44e04e8aa434cfb93ead258fc
"@react-native/gradle-plugin@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/gradle-plugin@npm:0.84.0"
checksum: 10/161c956b010a63f495c6d5fbe90446f7f4b71a584e315b0e2244cc8084f43241445650e90ab816187071fff8253481156c20d94251620786ad5076a7bf9cf622
languageName: node
linkType: hard
@@ -4159,6 +4177,13 @@ __metadata:
languageName: node
linkType: hard
"@react-native/js-polyfills@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/js-polyfills@npm:0.84.0"
checksum: 10/5b2927aee2f7106cfe2531f9fce4ef1563c207223a34a6ec2aec886b475b78e507a8dd23a9e2c9a0afafbaff56a53e23dec93c6b4dc14b224d026bbed4304d97
languageName: node
linkType: hard
"@react-native/metro-babel-transformer@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/metro-babel-transformer@npm:0.83.0"
@@ -4185,10 +4210,10 @@ __metadata:
languageName: node
linkType: hard
"@react-native/normalize-colors@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/normalize-colors@npm:0.83.0"
checksum: 10/84e3122bd0292ae9ee61a6d2997119196a5dc128ede7770a2a70e3b9de1b78f5f2a10b44dc683ce2652ad8f45767f249a74d1a86f7657a5dae7a377790512f68
"@react-native/normalize-colors@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/normalize-colors@npm:0.84.0"
checksum: 10/038a4687c023b4799126efc4b2d8b91abdb806fdc90af5b749b552b2c311d7459893f6c583941c1baed1c906ab437804916da2cdfc666a431c98482ddb62672a
languageName: node
linkType: hard
@@ -4199,9 +4224,9 @@ __metadata:
languageName: node
linkType: hard
"@react-native/virtualized-lists@npm:0.83.0":
version: 0.83.0
resolution: "@react-native/virtualized-lists@npm:0.83.0"
"@react-native/virtualized-lists@npm:0.84.0":
version: 0.84.0
resolution: "@react-native/virtualized-lists@npm:0.84.0"
dependencies:
invariant: "npm:^2.2.4"
nullthrows: "npm:^1.1.1"
@@ -4212,7 +4237,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/b51a3c0c110b6d259b6cf25fa4d143b03cbd6b637e5b33ea07453c0aa12a061e2f861a67c1b4089f96817ef46a1bd3709ecae3133026e92b77979b96a08bff06
checksum: 10/9a701e31a2912610fe6ba38d8d0aa255e559719f1c4a416073cef2b6c47ea0b9795fdef29738e9faeaaa670788a50d7a4f7db3704954b4a24e99111d16fedcc5
languageName: node
linkType: hard
@@ -10216,8 +10241,8 @@ __metadata:
nyc: "npm:^15.1.0"
p-iteration: "npm:^1.1.8"
pngjs: "npm:^3.4.0"
react: "npm:19.2.0"
react-native: "npm:0.83.0"
react: "npm:19.2.3"
react-native: "npm:0.84.0"
react-native-launch-arguments: "npm:^4.0.2"
react-native-permissions: "npm:^5.2.1"
react-native-webview: "npm:^13.13.1"
@@ -10279,7 +10304,7 @@ __metadata:
prettier: "npm:^3.1.1"
promisify-child-process: "npm:^4.1.2"
proper-lockfile: "npm:^3.0.2"
react-native: "npm:0.83.0"
react-native: "npm:0.84.0"
react-native-codegen: "npm:^0.0.8"
resolve-from: "npm:^5.0.0"
sanitize-filename: "npm:^1.6.1"
@@ -11743,8 +11768,8 @@ __metadata:
detox: "workspace:*"
fs-extra: "npm:^9.1.0"
jest: "npm:^30.0.3"
react: "npm:19.2.0"
react-native: "npm:0.83.0"
react: "npm:19.2.3"
react-native: "npm:0.84.0"
react-test-renderer: "npm:19.2.0"
ts-jest: "npm:^29.4.0"
tslib: "npm:^2.0.3"
@@ -13814,10 +13839,10 @@ __metadata:
languageName: node
linkType: hard
"hermes-compiler@npm:0.14.0":
version: 0.14.0
resolution: "hermes-compiler@npm:0.14.0"
checksum: 10/880a5570770a977731f98fe2bedf5b7079e2e851a16e08ec89aeaa1de552e885734de3be513bc5633e5415fd1ac497b7b630aab75a46c359a5d30a243a1dcf11
"hermes-compiler@npm:250829098.0.7":
version: 250829098.0.7
resolution: "hermes-compiler@npm:250829098.0.7"
checksum: 10/229cfee06c0e3205f93e1b8ac1782b852cd8ef190b4e3a2ad69f953b470facf0cca3c5986aa499a752802b714a82960d69b9a43dacaa056bd3998db7553b6c36
languageName: node
linkType: hard
@@ -22230,18 +22255,18 @@ __metadata:
languageName: node
linkType: hard
"react-native@npm:0.83.0":
version: 0.83.0
resolution: "react-native@npm:0.83.0"
"react-native@npm:0.84.0":
version: 0.84.0
resolution: "react-native@npm:0.84.0"
dependencies:
"@jest/create-cache-key-function": "npm:^29.7.0"
"@react-native/assets-registry": "npm:0.83.0"
"@react-native/codegen": "npm:0.83.0"
"@react-native/community-cli-plugin": "npm:0.83.0"
"@react-native/gradle-plugin": "npm:0.83.0"
"@react-native/js-polyfills": "npm:0.83.0"
"@react-native/normalize-colors": "npm:0.83.0"
"@react-native/virtualized-lists": "npm:0.83.0"
"@react-native/assets-registry": "npm:0.84.0"
"@react-native/codegen": "npm:0.84.0"
"@react-native/community-cli-plugin": "npm:0.84.0"
"@react-native/gradle-plugin": "npm:0.84.0"
"@react-native/js-polyfills": "npm:0.84.0"
"@react-native/normalize-colors": "npm:0.84.0"
"@react-native/virtualized-lists": "npm:0.84.0"
abort-controller: "npm:^3.0.0"
anser: "npm:^1.4.9"
ansi-regex: "npm:^5.0.0"
@@ -22250,8 +22275,7 @@ __metadata:
base64-js: "npm:^1.5.1"
commander: "npm:^12.0.0"
flow-enums-runtime: "npm:^0.0.6"
glob: "npm:^7.1.1"
hermes-compiler: "npm:0.14.0"
hermes-compiler: "npm:250829098.0.7"
invariant: "npm:^2.2.4"
jest-environment-node: "npm:^29.7.0"
memoize-one: "npm:^5.0.0"
@@ -22266,18 +22290,19 @@ __metadata:
scheduler: "npm:0.27.0"
semver: "npm:^7.1.3"
stacktrace-parser: "npm:^0.1.10"
tinyglobby: "npm:^0.2.15"
whatwg-fetch: "npm:^3.0.0"
ws: "npm:^7.5.10"
yargs: "npm:^17.6.2"
peerDependencies:
"@types/react": ^19.1.1
react: ^19.2.0
react: ^19.2.3
peerDependenciesMeta:
"@types/react":
optional: true
bin:
react-native: cli.js
checksum: 10/78454fa89c2f5c8794d8d04c099c6d5f43f569696b6e04afb927181bcdcc73646921e428bd29b32a79c94678d572ec00c5d3355e7a43fa94d58065eed4ba78f2
checksum: 10/f1ae3cd7f9581d0ee516bfbbdc705ea7099d025a5701d997bf21e0a158c630ab30edf03140e64128f33e1a99be7f607e53a9d4277723d73239e14a7cd7193eaf
languageName: node
linkType: hard
@@ -22382,10 +22407,10 @@ __metadata:
languageName: node
linkType: hard
"react@npm:19.2.0":
version: 19.2.0
resolution: "react@npm:19.2.0"
checksum: 10/e13bcdb8e994c3cfa922743cb75ca8deb60531bf02f584d2d8dab940a8132ce8a2e6ef16f8ed7f372b4072e7a7eeff589b2812dabbedfa73e6e46201dac8a9d0
"react@npm:19.2.3":
version: 19.2.3
resolution: "react@npm:19.2.3"
checksum: 10/d16b7f35c0d35a56f63d9d1693819762e4abc479c57dd6310298920bed3820fcec7e17a99d44983416d8f5049143ea45b8005d3ab8324bae8973224400502b08
languageName: node
linkType: hard