<lambda>null1 package expo.modules.test.core
2 
3 import android.content.Context
4 import android.os.Bundle
5 import androidx.test.core.app.ApplicationProvider
6 import com.facebook.react.bridge.ReactApplicationContext
7 import expo.modules.core.interfaces.services.EventEmitter
8 import expo.modules.kotlin.AppContext
9 import expo.modules.kotlin.ModuleHolder
10 import expo.modules.kotlin.modules.Module
11 import io.mockk.MockK
12 import io.mockk.MockKGateway
13 import io.mockk.every
14 import io.mockk.mockk
15 import io.mockk.spyk
16 import java.lang.ref.WeakReference
17 import java.lang.reflect.Proxy
18 import kotlin.reflect.KClass
19 
20 /**
21  * This class shouldn't be used directly. Instead use
22  * ```kotlin
23  * ModuleMock.createMock(MyModuleTestInterface::class, MyModule()) {
24  *   // module test code here
25  * }
26  * ```
27  */
28 data class ModuleMock<TestInterfaceType : Any, ModuleType : Module>(
29   val testInterface: TestInterfaceType,
30   val appContext: AppContext,
31   val eventEmitter: EventEmitter,
32   val moduleSpy: ModuleType
33 ) {
34   companion object {
35     /**
36      * This overload shouldn't be used directly. Instead use
37      * the inline overload with `block` being the last argument:
38      * `ModuleMock.createMock(..., block: (...) -> Unit)` instead
39      */
40     fun <TestInterfaceType : Any, ModuleType : Module> createMock(
41       moduleTestInterface: KClass<TestInterfaceType>,
42       module: ModuleType,
43       customAppContext: AppContext? = null,
44       customEventEmitter: EventEmitter? = null
45     ): ModuleMock<TestInterfaceType, ModuleType> {
46       val appContext = prepareMockAppContext(customAppContext)
47       val eventEmitter: EventEmitter = customEventEmitter ?: mockk(relaxed = true)
48 
49       // prepare module spy
50       val moduleSpy = convertToSpy(module, recordPrivateCalls = true)
51       every { moduleSpy getProperty "appContext" } returns appContext
52       every { moduleSpy.sendEvent(any(), any<Bundle>()) } answers { call ->
53         val (eventName, eventBody) = call.invocation.args
54         eventEmitter.emit(eventName as String, eventBody as? Bundle)
55       }
56 
57       val holder = ModuleHolder(moduleSpy)
58       val moduleControllerImpl = ModuleControllerImpl(holder)
59 
60       val invocationHandler = ModuleMockInvocationHandler(
61         moduleTestInterface,
62         moduleControllerImpl,
63         holder
64       )
65       @Suppress("UNCHECKED_CAST")
66       return ModuleMock(
67         Proxy
68           .newProxyInstance(
69             moduleTestInterface.java.classLoader,
70             arrayOf(moduleTestInterface.java, ModuleController::class.java),
71             invocationHandler
72           ) as TestInterfaceType,
73         appContext,
74         eventEmitter,
75         moduleSpy
76       )
77     }
78 
79     /**
80      * Executes the given [block] in the mocked module scope.
81      * Example usage:
82      * ```kotlin
83      * ModuleMock.createMock(MyModuleTestInterface::class, MyModule()) {
84      *   every { moduleSpy.someModulePrivateFn() } returns 5
85      *   val result = module.someFunctionAsync()
86      *   assertEquals(result, 5)
87      * }
88      * ```
89      */
90     inline fun <TestInterfaceType : Any, ModuleType : Module> createMock(
91       moduleTestInterface: KClass<TestInterfaceType>,
92       module: ModuleType,
93       autoOnCreate: Boolean = true,
94       customAppContext: AppContext? = null,
95       customEventEmitter: EventEmitter? = null,
96       block: ModuleMockHolder<TestInterfaceType, ModuleType>.() -> Unit
97     ) {
98       val (mock, appContext, eventEmitter, moduleSpy) = createMock(
99         moduleTestInterface,
100         module,
101         customAppContext,
102         customEventEmitter
103       )
104       val controller = mock as ModuleController
105       val holder = ModuleMockHolder<TestInterfaceType, ModuleType>(
106         mock, controller, appContext, eventEmitter, moduleSpy
107       )
108 
109       if (autoOnCreate) {
110         controller.onCreate()
111       }
112 
113       block.invoke(holder)
114     }
115   }
116 }
117 
prepareMockAppContextnull118 private fun prepareMockAppContext(customAppContext: AppContext?): AppContext {
119   val reactContext = ReactApplicationContext(ApplicationProvider.getApplicationContext<Context>())
120   val appContext = customAppContext ?: AppContext(
121     modulesProvider = mockk(relaxed = true),
122     legacyModuleRegistry = mockk(relaxed = true),
123     reactContextHolder = WeakReference(reactContext)
124   )
125 
126   // as AppContext holds only weak reference to Android Context which can be destroyed too early
127   // we need to override it to return actual strong reference (held by mockk internals)
128   val appContextSpy = convertToSpy(appContext)
129   every { appContextSpy getProperty "reactContext" } returns reactContext
130   every { appContextSpy getProperty "hasActiveReactInstance" } returns true
131   return appContextSpy
132 }
133 
134 /**
135  * Creates a spy from a given object or returns it as-is if it's already a spy
136  */
convertToSpynull137 private fun <T : Any> convertToSpy(obj: T, recordPrivateCalls: Boolean = false): T =
138   MockK.useImpl {
139     return@useImpl if (MockKGateway.implementation().mockTypeChecker.isSpy(obj)) {
140       obj
141     } else {
142       // this is actually spyk<T>(obj) but without syntax sugar
143       // because we're already inside MockK.useImpl { } which is part of that sugar
144       MockKGateway.implementation().mockFactory.spyk(
145         mockType = null, // this should be null if objToCopy is provided
146         objToCopy = obj,
147         name = null,
148         moreInterfaces = emptyArray(),
149         recordPrivateCalls = recordPrivateCalls
150       )
151     }
152   }
153