diff --git a/apps/example/__tests__/nitro.views.harness.tsx b/apps/example/__tests__/nitro.views.harness.tsx index 120bbef9e..4de631acf 100644 --- a/apps/example/__tests__/nitro.views.harness.tsx +++ b/apps/example/__tests__/nitro.views.harness.tsx @@ -161,6 +161,21 @@ const RENDER_TIMEOUT = 4_000 const SUPPORTS_NATIVE_VIEW_RECYCLING = Platform.OS === 'ios' || Number(Platform.Version) >= 28 +async function waitForRecyclableViewRemoval( + view: RecyclableTestViewRef, + testID: string, + expectedOnDropViewCount: number, + expectedPrepareForRecycleCount: number +): Promise { + await waitUntil( + () => + screen.queryByTestId(testID) === null && + view.getOnDropViewCount() === expectedOnDropViewCount && + view.getPrepareForRecycleCount() === expectedPrepareForRecycleCount, + { timeout: RENDER_TIMEOUT } + ) +} + describe('TestView', () => { it('renders with native props, layout, pixels, methods, and callbacks', async () => { const viewRef = deferred() @@ -321,11 +336,14 @@ describe('TestView', () => { expect(view.nativeDefaultValue).toBe(42) expect(view.getNativeDefaultValueSetterCallCount()).toBe(0) + const updatedViewRef = deferred() await renderResult.rerender( + updatedViewRef.resolve(updatedView) + )} isBlue={true} hasBeenCalled={false} colorScheme="dark" @@ -333,6 +351,7 @@ describe('TestView', () => { /> ) + await updatedViewRef.promise expect(view.isBlue).toBe(true) expect(view.getIsBlueSetterCallCount()).toBe(2) expect(view.nativeDefaultValue).toBe(42) @@ -363,11 +382,14 @@ describe('TestView', () => { expect(view.getIsBlueSetterCallCount()).toBe(1) expect(view.getNativeDefaultValueSetterCallCount()).toBe(1) + const nativeDefaultUpdatedRef = deferred() await renderResult.rerender( + nativeDefaultUpdatedRef.resolve(updatedView) + )} isBlue={false} hasBeenCalled={false} colorScheme="dark" @@ -376,6 +398,12 @@ describe('TestView', () => { /> ) + await nativeDefaultUpdatedRef.promise + + const isBlueUpdatedRef = deferred() + const isBlueUpdatedHybridRef = callback((updatedView: TestViewRef) => + isBlueUpdatedRef.resolve(updatedView) + ) expect(view.isBlue).toBe(false) expect(view.nativeDefaultValue).toBe(2) expect(view.getIsBlueSetterCallCount()).toBe(1) @@ -385,7 +413,7 @@ describe('TestView', () => { { /> ) + await isBlueUpdatedRef.promise expect(view.isBlue).toBe(true) expect(view.nativeDefaultValue).toBe(2) expect(view.getIsBlueSetterCallCount()).toBe(2) @@ -404,7 +433,7 @@ describe('TestView', () => { { renderResult.unmount() await waitUntil( - () => screen.queryByTestId('test-view-lifecycle') === null, + () => + screen.queryByTestId('test-view-lifecycle') === null && + firstView.getOnDropViewCount() === initialOnDropViewCount + 1, { timeout: RENDER_TIMEOUT } ) + expect(screen.queryByTestId('test-view-lifecycle')).toBeNull() expect(firstView.getOnDropViewCount()).toBe(initialOnDropViewCount + 1) const secondRef = deferred() @@ -528,13 +560,17 @@ describe('multiple RecyclableTestViews', () => { expectRed(initialFirstCapture.pixelCoverage) expectRed(initialSecondCapture.pixelCoverage) + const updatedFirstRef = deferred() + const updatedFirstHybridRef = callback((view: RecyclableTestViewRef) => + updatedFirstRef.resolve(view) + ) await renderResult.rerender( { ) + await updatedFirstRef.promise expect(firstView.isBlue).toBe(true) expect(secondView.isBlue).toBe(false) expect(firstView.getOnDropViewCount()).toBe(firstOnDropViewCount) @@ -579,6 +616,12 @@ describe('multiple RecyclableTestViews', () => { ) + await waitForRecyclableViewRemoval( + firstView, + 'isolated-recyclable-view-first', + firstOnDropViewCount + 1, + firstPrepareForRecycleCount + (SUPPORTS_NATIVE_VIEW_RECYCLING ? 1 : 0) + ) expect(screen.queryByTestId('isolated-recyclable-view-first')).toBeNull() expect(firstView.getOnDropViewCount()).toBe(firstOnDropViewCount + 1) expect(firstView.getPrepareForRecycleCount()).toBe( @@ -597,6 +640,7 @@ describe('multiple RecyclableTestViews', () => { expectRed(mountedSiblingCapture.pixelCoverage) const remountedFirstRef = deferred() + const remountedFirstLayout = deferred() await renderResult.rerender( { style={INITIAL_SIZE} hybridRef={callback((view) => remountedFirstRef.resolve(view))} isBlue={true} + onLayout={({ nativeEvent }) => + remountedFirstLayout.resolve(nativeEvent.layout) + } /> { ) const remountedFirstView = await remountedFirstRef.promise + await remountedFirstLayout.promise expect(remountedFirstView.equals(firstView)).toBe( SUPPORTS_NATIVE_VIEW_RECYCLING ) @@ -750,6 +798,12 @@ describe('RecyclableTestView', () => { expectBlue(initialCapture.pixelCoverage) await renderResult.rerender() + await waitForRecyclableViewRemoval( + firstView, + 'recyclable-view-lifecycle', + initialOnDropViewCount + 1, + initialPrepareForRecycleCount + (SUPPORTS_NATIVE_VIEW_RECYCLING ? 1 : 0) + ) expect(screen.queryByTestId('recyclable-view-lifecycle')).toBeNull() expect(firstView.getOnDropViewCount()).toBe(initialOnDropViewCount + 1) expect(firstView.getPrepareForRecycleCount()).toBe( @@ -794,6 +848,12 @@ describe('RecyclableTestView', () => { expectRed(secondCapture.pixelCoverage) await renderResult.rerender() + await waitForRecyclableViewRemoval( + secondView, + 'recyclable-view-lifecycle', + SUPPORTS_NATIVE_VIEW_RECYCLING ? initialOnDropViewCount + 2 : 1, + SUPPORTS_NATIVE_VIEW_RECYCLING ? initialPrepareForRecycleCount + 2 : 0 + ) expect(screen.queryByTestId('recyclable-view-lifecycle')).toBeNull() expect(secondView.getOnDropViewCount()).toBe( SUPPORTS_NATIVE_VIEW_RECYCLING ? initialOnDropViewCount + 2 : 1 diff --git a/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridRecyclableTestView.kt b/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridRecyclableTestView.kt index ec50063bd..63146b8b6 100644 --- a/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridRecyclableTestView.kt +++ b/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridRecyclableTestView.kt @@ -7,6 +7,7 @@ import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.views.RecyclableView +import java.util.concurrent.atomic.AtomicInteger @Keep @DoNotStrip @@ -17,9 +18,9 @@ class HybridRecyclableTestView( // View override val view: View = View(context) private var isRecycled = false - private var invalidLifecycleOrderCount = 0.0 - private var onDropViewCount = 0.0 - private var prepareForRecycleCount = 0.0 + private val invalidLifecycleOrderCount = AtomicInteger() + private val onDropViewCount = AtomicInteger() + private val prepareForRecycleCount = AtomicInteger() private var nativeDefaultValueSetterCallCount = 0.0 private var nativeDefaultValueStorage: Double? = 42.0 @@ -40,15 +41,15 @@ class HybridRecyclableTestView( } override fun onDropView() { - onDropViewCount += 1 Log.i(TAG, "View dropped!") + onDropViewCount.incrementAndGet() } - override fun getOnDropViewCount(): Double = onDropViewCount + override fun getOnDropViewCount(): Double = onDropViewCount.get().toDouble() - override fun getInvalidLifecycleOrderCount(): Double = invalidLifecycleOrderCount + override fun getInvalidLifecycleOrderCount(): Double = invalidLifecycleOrderCount.get().toDouble() - override fun getPrepareForRecycleCount(): Double = prepareForRecycleCount + override fun getPrepareForRecycleCount(): Double = prepareForRecycleCount.get().toDouble() override fun getNativeDefaultValueSetterCallCount(): Double = nativeDefaultValueSetterCallCount @@ -58,13 +59,13 @@ class HybridRecyclableTestView( // Recycling conformance override fun prepareForRecycle() { - if (onDropViewCount != prepareForRecycleCount + 1) { - invalidLifecycleOrderCount += 1 + if (onDropViewCount.get() != prepareForRecycleCount.get() + 1) { + invalidLifecycleOrderCount.incrementAndGet() } - prepareForRecycleCount += 1 nativeDefaultValueStorage = 42.0 nativeDefaultValueSetterCallCount = 0.0 view.setBackgroundColor(Color.YELLOW) isRecycled = true + prepareForRecycleCount.incrementAndGet() } } diff --git a/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestView.kt b/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestView.kt index 8b6141cad..e3f1f6e2a 100644 --- a/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestView.kt +++ b/packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestView.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.uimanager.ThemedReactContext +import java.util.concurrent.atomic.AtomicInteger @Keep @DoNotStrip @@ -13,7 +14,7 @@ class HybridTestView( ) : HybridTestViewSpec() { // View override val view: View = View(context) - private var onDropViewCount = 0.0 + private val onDropViewCount = AtomicInteger() private var isBlueSetterCallCount = 0.0 private var nativeDefaultValueSetterCallCount = 0.0 @@ -35,7 +36,7 @@ class HybridTestView( } // Methods - override fun getOnDropViewCount(): Double = onDropViewCount + override fun getOnDropViewCount(): Double = onDropViewCount.get().toDouble() override fun getIsBlueSetterCallCount(): Double = isBlueSetterCallCount @@ -47,6 +48,6 @@ class HybridTestView( } override fun onDropView() { - onDropViewCount += 1 + onDropViewCount.incrementAndGet() } } diff --git a/packages/react-native-nitro-test/ios/HybridRecyclableTestView.swift b/packages/react-native-nitro-test/ios/HybridRecyclableTestView.swift index 1e9700541..4e1d0eede 100644 --- a/packages/react-native-nitro-test/ios/HybridRecyclableTestView.swift +++ b/packages/react-native-nitro-test/ios/HybridRecyclableTestView.swift @@ -11,6 +11,7 @@ import UIKit class HybridRecyclableTestView: HybridRecyclableTestViewSpec, RecyclableView { // UIView var view: UIView = UIView() + private let lifecycleLock = NSLock() private var isRecycled = false private var invalidLifecycleOrderCount: Double = 0 private var onDropViewCount: Double = 0 @@ -37,20 +38,22 @@ class HybridRecyclableTestView: HybridRecyclableTestViewSpec, RecyclableView { } func onDropView() { - onDropViewCount += 1 print("View dropped!") + lifecycleLock.withLock { + onDropViewCount += 1 + } } func getOnDropViewCount() throws -> Double { - return onDropViewCount + return lifecycleLock.withLock { onDropViewCount } } func getInvalidLifecycleOrderCount() throws -> Double { - return invalidLifecycleOrderCount + return lifecycleLock.withLock { invalidLifecycleOrderCount } } func getPrepareForRecycleCount() throws -> Double { - return prepareForRecycleCount + return lifecycleLock.withLock { prepareForRecycleCount } } func getNativeDefaultValueSetterCallCount() throws -> Double { @@ -63,13 +66,17 @@ class HybridRecyclableTestView: HybridRecyclableTestViewSpec, RecyclableView { // Recycling conformance func prepareForRecycle() { - if onDropViewCount != prepareForRecycleCount + 1 { - invalidLifecycleOrderCount += 1 - } - prepareForRecycleCount += 1 nativeDefaultValueStorage = 42 nativeDefaultValueSetterCallCount = 0 view.backgroundColor = .yellow isRecycled = true + + lifecycleLock.withLock { + if onDropViewCount != prepareForRecycleCount + 1 { + invalidLifecycleOrderCount += 1 + } + prepareForRecycleCount += 1 + } } + } diff --git a/packages/react-native-nitro-test/ios/HybridTestView.swift b/packages/react-native-nitro-test/ios/HybridTestView.swift index 571a5dbaf..56ec14ebb 100644 --- a/packages/react-native-nitro-test/ios/HybridTestView.swift +++ b/packages/react-native-nitro-test/ios/HybridTestView.swift @@ -11,6 +11,7 @@ import UIKit class HybridTestView: HybridTestViewSpec { // UIView var view: UIView = UIView() + private let lifecycleLock = NSLock() private var onDropViewCount: Double = 0 private var isBlueSetterCallCount: Double = 0 private var nativeDefaultValueSetterCallCount: Double = 0 @@ -33,7 +34,7 @@ class HybridTestView: HybridTestViewSpec { // Methods func getOnDropViewCount() throws -> Double { - return onDropViewCount + return lifecycleLock.withLock { onDropViewCount } } func getIsBlueSetterCallCount() throws -> Double { @@ -50,6 +51,9 @@ class HybridTestView: HybridTestViewSpec { } func onDropView() { - onDropViewCount += 1 + lifecycleLock.withLock { + onDropViewCount += 1 + } } + }