Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions apps/example/__tests__/nitro.views.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<TestViewRef>()
Expand Down Expand Up @@ -321,18 +336,22 @@ describe('TestView', () => {
expect(view.nativeDefaultValue).toBe(42)
expect(view.getNativeDefaultValueSetterCallCount()).toBe(0)

const updatedViewRef = deferred<TestViewRef>()
await renderResult.rerender(
<TestView
testID="test-view-native-default"
style={INITIAL_SIZE}
hybridRef={stableHybridRef}
hybridRef={callback((updatedView) =>
updatedViewRef.resolve(updatedView)
)}
isBlue={true}
hasBeenCalled={false}
colorScheme="dark"
someCallback={stableSomeCallback}
/>
)

await updatedViewRef.promise
expect(view.isBlue).toBe(true)
expect(view.getIsBlueSetterCallCount()).toBe(2)
expect(view.nativeDefaultValue).toBe(42)
Expand Down Expand Up @@ -363,11 +382,14 @@ describe('TestView', () => {
expect(view.getIsBlueSetterCallCount()).toBe(1)
expect(view.getNativeDefaultValueSetterCallCount()).toBe(1)

const nativeDefaultUpdatedRef = deferred<TestViewRef>()
await renderResult.rerender(
<TestView
testID="test-view-setter-counts"
style={INITIAL_SIZE}
hybridRef={stableHybridRef}
hybridRef={callback((updatedView) =>
nativeDefaultUpdatedRef.resolve(updatedView)
)}
isBlue={false}
hasBeenCalled={false}
colorScheme="dark"
Expand All @@ -376,6 +398,12 @@ describe('TestView', () => {
/>
)

await nativeDefaultUpdatedRef.promise

const isBlueUpdatedRef = deferred<TestViewRef>()
const isBlueUpdatedHybridRef = callback((updatedView: TestViewRef) =>
isBlueUpdatedRef.resolve(updatedView)
)
expect(view.isBlue).toBe(false)
expect(view.nativeDefaultValue).toBe(2)
expect(view.getIsBlueSetterCallCount()).toBe(1)
Expand All @@ -385,7 +413,7 @@ describe('TestView', () => {
<TestView
testID="test-view-setter-counts"
style={INITIAL_SIZE}
hybridRef={stableHybridRef}
hybridRef={isBlueUpdatedHybridRef}
isBlue={true}
hasBeenCalled={false}
colorScheme="dark"
Expand All @@ -394,6 +422,7 @@ describe('TestView', () => {
/>
)

await isBlueUpdatedRef.promise
expect(view.isBlue).toBe(true)
expect(view.nativeDefaultValue).toBe(2)
expect(view.getIsBlueSetterCallCount()).toBe(2)
Expand All @@ -404,7 +433,7 @@ describe('TestView', () => {
<TestView
testID="test-view-setter-counts"
style={RESIZED_SIZE}
hybridRef={stableHybridRef}
hybridRef={isBlueUpdatedHybridRef}
isBlue={true}
hasBeenCalled={false}
colorScheme="dark"
Expand Down Expand Up @@ -443,9 +472,12 @@ 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<TestViewRef>()
Expand Down Expand Up @@ -528,13 +560,17 @@ describe('multiple RecyclableTestViews', () => {
expectRed(initialFirstCapture.pixelCoverage)
expectRed(initialSecondCapture.pixelCoverage)

const updatedFirstRef = deferred<RecyclableTestViewRef>()
const updatedFirstHybridRef = callback((view: RecyclableTestViewRef) =>
updatedFirstRef.resolve(view)
)
await renderResult.rerender(
<View style={{ flexDirection: 'row' }}>
<RecyclableTestView
key="first"
testID="isolated-recyclable-view-first"
style={INITIAL_SIZE}
hybridRef={firstHybridRef}
hybridRef={updatedFirstHybridRef}
isBlue={true}
/>
<RecyclableTestView
Expand All @@ -547,6 +583,7 @@ describe('multiple RecyclableTestViews', () => {
</View>
)

await updatedFirstRef.promise
expect(firstView.isBlue).toBe(true)
expect(secondView.isBlue).toBe(false)
expect(firstView.getOnDropViewCount()).toBe(firstOnDropViewCount)
Expand Down Expand Up @@ -579,6 +616,12 @@ describe('multiple RecyclableTestViews', () => {
</View>
)

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(
Expand All @@ -597,6 +640,7 @@ describe('multiple RecyclableTestViews', () => {
expectRed(mountedSiblingCapture.pixelCoverage)

const remountedFirstRef = deferred<RecyclableTestViewRef>()
const remountedFirstLayout = deferred<LayoutRectangle>()
await renderResult.rerender(
<View style={{ flexDirection: 'row' }}>
<RecyclableTestView
Expand All @@ -605,6 +649,9 @@ describe('multiple RecyclableTestViews', () => {
style={INITIAL_SIZE}
hybridRef={callback((view) => remountedFirstRef.resolve(view))}
isBlue={true}
onLayout={({ nativeEvent }) =>
remountedFirstLayout.resolve(nativeEvent.layout)
}
/>
<RecyclableTestView
key="second"
Expand All @@ -617,6 +664,7 @@ describe('multiple RecyclableTestViews', () => {
)

const remountedFirstView = await remountedFirstRef.promise
await remountedFirstLayout.promise
expect(remountedFirstView.equals(firstView)).toBe(
SUPPORTS_NATIVE_VIEW_RECYCLING
)
Expand Down Expand Up @@ -750,6 +798,12 @@ describe('RecyclableTestView', () => {
expectBlue(initialCapture.pixelCoverage)

await renderResult.rerender(<View />)
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(
Expand Down Expand Up @@ -794,6 +848,12 @@ describe('RecyclableTestView', () => {
expectRed(secondCapture.pixelCoverage)

await renderResult.rerender(<View />)
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -35,7 +36,7 @@ class HybridTestView(
}

// Methods
override fun getOnDropViewCount(): Double = onDropViewCount
override fun getOnDropViewCount(): Double = onDropViewCount.get().toDouble()

override fun getIsBlueSetterCallCount(): Double = isBlueSetterCallCount

Expand All @@ -47,6 +48,6 @@ class HybridTestView(
}

override fun onDropView() {
onDropViewCount += 1
onDropViewCount.incrementAndGet()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
}

}
8 changes: 6 additions & 2 deletions packages/react-native-nitro-test/ios/HybridTestView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +34,7 @@ class HybridTestView: HybridTestViewSpec {

// Methods
func getOnDropViewCount() throws -> Double {
return onDropViewCount
return lifecycleLock.withLock { onDropViewCount }
}

func getIsBlueSetterCallCount() throws -> Double {
Expand All @@ -50,6 +51,9 @@ class HybridTestView: HybridTestViewSpec {
}

func onDropView() {
onDropViewCount += 1
lifecycleLock.withLock {
onDropViewCount += 1
}
}

}
Loading