expo-modules-test-core 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # expo-modules-test-core
2
+
3
+ Goal of this module is to provide utility methods that will help developers easily write unit tests for native code in expo modules.
4
+
5
+ Module is purely native and should be included and used exclusively in tests targets. It defines and exposes default set of dependencies used for testing in Android.
@@ -0,0 +1,100 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'maven-publish'
4
+
5
+ group = 'org.unimodules'
6
+ version = '0.8.0'
7
+
8
+ buildscript {
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ if (expoModulesCorePlugin.exists()) {
11
+ apply from: expoModulesCorePlugin
12
+ applyKotlinExpoModulesCorePlugin()
13
+ }
14
+
15
+ // Simple helper that allows the root project to override versions declared by this library.
16
+ ext.safeExtGet = { prop, fallback ->
17
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18
+ }
19
+
20
+ // Ensures backward compatibility
21
+ ext.getKotlinVersion = {
22
+ if (ext.has("kotlinVersion")) {
23
+ ext.kotlinVersion()
24
+ } else {
25
+ ext.safeExtGet("kotlinVersion", "1.6.10")
26
+ }
27
+ }
28
+
29
+ repositories {
30
+ mavenCentral()
31
+ }
32
+
33
+ dependencies {
34
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
35
+ }
36
+ }
37
+
38
+ // Creating sources with comments
39
+ task androidSourcesJar(type: Jar) {
40
+ classifier = 'sources'
41
+ from android.sourceSets.main.java.srcDirs
42
+ }
43
+
44
+ afterEvaluate {
45
+ publishing {
46
+ publications {
47
+ release(MavenPublication) {
48
+ from components.release
49
+ // Add additional sourcesJar to artifacts
50
+ artifact(androidSourcesJar)
51
+ }
52
+ }
53
+ repositories {
54
+ maven {
55
+ url = mavenLocal().url
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ android {
62
+ compileSdkVersion safeExtGet("compileSdkVersion", 31)
63
+
64
+ compileOptions {
65
+ sourceCompatibility JavaVersion.VERSION_11
66
+ targetCompatibility JavaVersion.VERSION_11
67
+ }
68
+
69
+ kotlinOptions {
70
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
71
+ }
72
+
73
+ defaultConfig {
74
+ minSdkVersion safeExtGet("minSdkVersion", 21)
75
+ targetSdkVersion safeExtGet("targetSdkVersion", 31)
76
+ versionCode 3
77
+ versionName '0.8.0'
78
+ }
79
+ lintOptions {
80
+ abortOnError false
81
+ }
82
+ }
83
+
84
+ repositories {
85
+ mavenCentral()
86
+ }
87
+
88
+ dependencies {
89
+ implementation project(':expo-modules-core')
90
+ api 'androidx.test:core:1.4.0'
91
+ api 'junit:junit:4.13.1'
92
+ api 'io.mockk:mockk:1.12.0'
93
+ api "org.robolectric:robolectric:4.5.1"
94
+
95
+ //noinspection GradleDynamicVersion
96
+ implementation 'com.facebook.react:react-native:+'
97
+
98
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
99
+ implementation "org.jetbrains.kotlin:kotlin-reflect:${getKotlinVersion()}"
100
+ }
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest package="org.unimodules.test.core">
3
+
4
+ </manifest>
@@ -0,0 +1,34 @@
1
+ package expo.modules.test.core
2
+
3
+ import expo.modules.kotlin.ModuleHolder
4
+ import expo.modules.kotlin.events.EventName
5
+
6
+ interface ModuleController {
7
+ fun onCreate()
8
+ fun onDestroy()
9
+ fun onActivityEntersForeground()
10
+ fun onActivityEntersBackground()
11
+ fun onActivityDestroys()
12
+ }
13
+
14
+ class ModuleControllerImpl(private val holder: ModuleHolder) : ModuleController {
15
+ override fun onCreate() {
16
+ holder.post(EventName.MODULE_CREATE)
17
+ }
18
+
19
+ override fun onDestroy() {
20
+ holder.post(EventName.MODULE_DESTROY)
21
+ }
22
+
23
+ override fun onActivityEntersForeground() {
24
+ holder.post(EventName.ACTIVITY_ENTERS_FOREGROUND)
25
+ }
26
+
27
+ override fun onActivityEntersBackground() {
28
+ holder.post(EventName.ACTIVITY_ENTERS_BACKGROUND)
29
+ }
30
+
31
+ override fun onActivityDestroys() {
32
+ holder.post(EventName.ACTIVITY_DESTROYS)
33
+ }
34
+ }
@@ -0,0 +1,86 @@
1
+ package expo.modules.test.core
2
+
3
+ import android.os.Bundle
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import expo.modules.core.interfaces.services.EventEmitter
7
+ import expo.modules.kotlin.AppContext
8
+ import expo.modules.kotlin.ModuleHolder
9
+ import expo.modules.kotlin.modules.Module
10
+ import io.mockk.every
11
+ import io.mockk.mockk
12
+ import io.mockk.spyk
13
+ import java.lang.ref.WeakReference
14
+ import java.lang.reflect.Proxy
15
+ import kotlin.reflect.KClass
16
+
17
+ class ModuleMock {
18
+ companion object {
19
+ fun <T : Any> createMock(
20
+ moduleTestInterface: KClass<T>,
21
+ module: Module,
22
+ customAppContext: AppContext? = null,
23
+ customEventEmitter: EventEmitter? = null,
24
+ ): Triple<T, AppContext, EventEmitter> {
25
+ val context = ReactApplicationContext(ApplicationProvider.getApplicationContext())
26
+ val appContext = customAppContext ?: AppContext(
27
+ modulesProvider = mockk(relaxed = true),
28
+ legacyModuleRegistry = mockk(relaxed = true),
29
+ reactContextHolder = WeakReference(context)
30
+ )
31
+
32
+ val eventEmitter: EventEmitter = customEventEmitter ?: mockk(relaxed = true)
33
+
34
+ val moduleSpy = spyk(module)
35
+ every { moduleSpy getProperty "appContext" } returns appContext
36
+ every { moduleSpy.sendEvent(any(), any()) } answers { call ->
37
+ val (eventName, eventBody) = call.invocation.args
38
+ eventEmitter.emit(eventName as String, eventBody as? Bundle)
39
+ }
40
+
41
+ val holder = ModuleHolder(moduleSpy)
42
+ val moduleControllerImpl = ModuleControllerImpl(holder)
43
+
44
+ val invocationHandler = ModuleMockInvocationHandler(
45
+ moduleTestInterface,
46
+ moduleControllerImpl,
47
+ holder
48
+ )
49
+ @Suppress("UNCHECKED_CAST")
50
+ return Triple(
51
+ Proxy
52
+ .newProxyInstance(
53
+ moduleTestInterface.java.classLoader,
54
+ arrayOf(moduleTestInterface.java, ModuleController::class.java),
55
+ invocationHandler
56
+ ) as T,
57
+ appContext,
58
+ eventEmitter
59
+ )
60
+ }
61
+
62
+ inline fun <T : Any> createMock(
63
+ moduleTestInterface: KClass<T>,
64
+ module: Module,
65
+ autoOnCreate: Boolean = true,
66
+ customAppContext: AppContext? = null,
67
+ customEventEmitter: EventEmitter? = null,
68
+ block: ModuleMockHolder<T>.() -> Unit
69
+ ) {
70
+ val (mock, appContext, eventEmitter) = createMock(
71
+ moduleTestInterface,
72
+ module,
73
+ customAppContext,
74
+ customEventEmitter
75
+ )
76
+ val controller = mock as ModuleController
77
+ val holder = ModuleMockHolder<T>(mock, controller, appContext, eventEmitter)
78
+
79
+ if (autoOnCreate) {
80
+ controller.onCreate()
81
+ }
82
+
83
+ block.invoke(holder)
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,11 @@
1
+ package expo.modules.test.core
2
+
3
+ import expo.modules.core.interfaces.services.EventEmitter
4
+ import expo.modules.kotlin.AppContext
5
+
6
+ data class ModuleMockHolder<T>(
7
+ val module: T,
8
+ val controller: ModuleController,
9
+ val appContext: AppContext,
10
+ val eventEmitter: EventEmitter
11
+ )
@@ -0,0 +1,104 @@
1
+ package expo.modules.test.core
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import expo.modules.kotlin.ModuleHolder
5
+ import expo.modules.kotlin.Promise
6
+ import expo.modules.kotlin.types.JSTypeConverter
7
+ import java.lang.reflect.InvocationHandler
8
+ import java.lang.reflect.Method
9
+ import kotlin.reflect.KClass
10
+
11
+ /**
12
+ * The promise rejection will be converted into this exception.
13
+ */
14
+ class TestCodedException(
15
+ code: String,
16
+ message: String?,
17
+ cause: Throwable?
18
+ ) : Exception("[$code] $message", cause)
19
+
20
+ /**
21
+ * Mocked module invocation handler which dispatches a call on test interface to the corresponding
22
+ * exported function or to the module controller if the method doesn't exist in the module definition.
23
+ *
24
+ * Methods mapping:
25
+ * function("name") { args: ArgsType -> return ReturnType } can be invoked using one of the following methods mapping rules:
26
+ * - [non-promise mapping] fun ModuleTestInterface.name(args: ArgsType): ReturnType
27
+ * - [promise mapping] fun ModuleTestInterface.name(args: ArgsType, promise: Promise): Unit
28
+ *
29
+ * function("name") { args: ArgsType, promise: Promise -> promise.resolve(ReturnType) } can be invoked using one of the following methods mapping rules:
30
+ * - [non-promise mapping] fun ModuleTestInterface.name(args: ArgsType): ReturnType
31
+ * - [promise mapping] fun ModuleTestInterface.name(args: ArgsType, promise: Promise): Unit
32
+ *
33
+ * In tests, the non-promise mapping should be preferred if possible.
34
+ * The promise mapping should be only used when dealing with native async code.
35
+ *
36
+ * In the non-promise mapping, rejection will be converted into exceptions.
37
+ * If you want to test if the method rejects, add the `@Throws` annotation to the `ModuleTestInterface` method you are testing.
38
+ * Otherwise, the exception will be wrapped in `UndeclaredThrowableException`.
39
+ */
40
+ class ModuleMockInvocationHandler<T : Any>(
41
+ private val moduleTestInterface: KClass<T>,
42
+ private val moduleController: ModuleController,
43
+ private val holder: ModuleHolder
44
+ ) : InvocationHandler {
45
+ override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
46
+ if (!holder.definition.methods.containsKey(method.name)) {
47
+ return method.invoke(moduleController, *(args ?: emptyArray()))
48
+ }
49
+
50
+ return callExportedFunction(method.name, args)
51
+ }
52
+
53
+ private fun callExportedFunction(methodName: String, args: Array<out Any>?): Any? {
54
+ val lastArg = args?.lastOrNull()
55
+ if (Promise::class.java.isInstance(lastArg)) {
56
+ promiseMappingCall(methodName, args!!.dropLast(1), lastArg as Promise)
57
+ return Unit
58
+ }
59
+
60
+ return nonPromiseMappingCall(methodName, args)
61
+ }
62
+
63
+ private fun nonPromiseMappingCall(methodName: String, args: Array<out Any>?): Any? {
64
+ val mockedPromise = PromiseMock()
65
+ holder.call(methodName, convertArgs(args?.asList() ?: emptyList()), mockedPromise)
66
+
67
+ when (mockedPromise.state) {
68
+ PromiseState.RESOLVED -> {
69
+ val moduleClassMethod = moduleTestInterface.members.firstOrNull { it.name == methodName }
70
+ ?: throw IllegalStateException("Module class method '$methodName' not found")
71
+
72
+ if (mockedPromise.resolveValue == null) {
73
+ if (moduleClassMethod.returnType.isMarkedNullable) {
74
+ return null
75
+ }
76
+
77
+ throw IllegalStateException("Method returns 'null' but the non-nullable type was expected")
78
+ }
79
+
80
+ if (!(moduleClassMethod.returnType.classifier as KClass<*>).isInstance(mockedPromise.resolveValue)) {
81
+ throw IllegalStateException("Illegal return type ${mockedPromise.resolveValue?.javaClass}, expected ${moduleClassMethod.returnType.classifier}.")
82
+ }
83
+
84
+ return mockedPromise.resolveValue
85
+ }
86
+ PromiseState.REJECTED ->
87
+ throw TestCodedException(
88
+ mockedPromise.rejectCode!!,
89
+ mockedPromise.rejectMessage,
90
+ mockedPromise.rejectThrowable
91
+ )
92
+ PromiseState.NONE, PromiseState.ILLEGAL ->
93
+ throw IllegalStateException("Illegal promise state '${mockedPromise.state}'")
94
+ }
95
+ }
96
+
97
+ private fun promiseMappingCall(methodName: String, args: List<Any>, promise: Promise) {
98
+ holder.call(methodName, convertArgs(args), promise)
99
+ }
100
+
101
+ private fun convertArgs(args: Iterable<Any?>): ReadableArray {
102
+ return JSTypeConverter.convertToJSValue(args, TestJSContainerProvider) as ReadableArray
103
+ }
104
+ }
@@ -0,0 +1,71 @@
1
+ // copied from expo-modules-core
2
+ package expo.modules.test.core
3
+
4
+ import expo.modules.kotlin.Promise
5
+
6
+ enum class PromiseState {
7
+ NONE,
8
+ REJECTED,
9
+ RESOLVED,
10
+ ILLEGAL
11
+ }
12
+
13
+ class PromiseMock : Promise {
14
+
15
+ var state = PromiseState.NONE
16
+
17
+ var resolveValueSet: Boolean = false
18
+ var resolveValue: Any? = null
19
+ set(value) {
20
+ this.resolveValueSet = true
21
+ field = value
22
+ }
23
+
24
+ var rejectCodeSet: Boolean = false
25
+ var rejectCode: String? = null
26
+ set(value) {
27
+ this.rejectCodeSet = true
28
+ field = value
29
+ }
30
+
31
+ var rejectMessageSet: Boolean = false
32
+ var rejectMessage: String? = null
33
+ set(value) {
34
+ this.rejectMessageSet = true
35
+ field = value
36
+ }
37
+
38
+ var rejectThrowableSet: Boolean = false
39
+ var rejectThrowable: Throwable? = null
40
+ set(value) {
41
+ this.rejectThrowableSet = true
42
+ field = value
43
+ }
44
+
45
+ override fun resolve(value: Any?) {
46
+ assertNotResolvedNorRejected()
47
+ state = PromiseState.RESOLVED
48
+ resolveValue = value
49
+ }
50
+
51
+ override fun reject(code: String, message: String?, cause: Throwable?) {
52
+ assertNotResolvedNorRejected()
53
+ state = PromiseState.REJECTED
54
+ rejectCode = code
55
+ rejectMessage = message
56
+ rejectThrowable = cause
57
+ }
58
+
59
+ private fun assertNotResolvedNorRejected() {
60
+ when (state) {
61
+ PromiseState.RESOLVED,
62
+ PromiseState.REJECTED,
63
+ PromiseState.ILLEGAL -> {
64
+ state = PromiseState.ILLEGAL
65
+ throw IllegalStateException("Cannot resolve same promise twice!")
66
+ }
67
+ else -> {
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,12 @@
1
+ package expo.modules.test.core
2
+
3
+ import com.facebook.react.bridge.JavaOnlyArray
4
+ import com.facebook.react.bridge.JavaOnlyMap
5
+ import com.facebook.react.bridge.WritableArray
6
+ import com.facebook.react.bridge.WritableMap
7
+ import expo.modules.kotlin.types.JSTypeConverter
8
+
9
+ object TestJSContainerProvider : JSTypeConverter.ContainerProvider {
10
+ override fun createMap(): WritableMap = JavaOnlyMap()
11
+ override fun createArray(): WritableArray = JavaOnlyArray()
12
+ }
@@ -0,0 +1,32 @@
1
+ package expo.modules.test.core
2
+
3
+ import org.junit.Assert
4
+
5
+ fun assertResolved(promise: PromiseMock) {
6
+ Assert.assertEquals(PromiseState.RESOLVED, promise.state)
7
+ }
8
+
9
+ fun assertRejected(promise: PromiseMock) {
10
+ Assert.assertEquals(PromiseState.REJECTED, promise.state)
11
+ }
12
+
13
+ inline fun <reified ResolveType> promiseResolved(promise: PromiseMock, with: (ResolveType) -> Unit) {
14
+ assertResolved(promise)
15
+ Assert.assertTrue(
16
+ "Promise resolved with incorrect type: ${ResolveType::class.simpleName}",
17
+ promise.resolveValue is ResolveType
18
+ )
19
+ with(promise.resolveValue as ResolveType)
20
+ }
21
+
22
+ fun promiseRejected(promise: PromiseMock, with: (PromiseMock) -> Unit) {
23
+ assertRejected(promise)
24
+ with(promise)
25
+ }
26
+
27
+ fun assertRejectedWithCode(promise: PromiseMock, rejectCode: String) {
28
+ promiseRejected(promise) {
29
+ Assert.assertTrue("Promise has no rejection code", it.rejectCodeSet)
30
+ Assert.assertEquals(it.rejectCode, rejectCode)
31
+ }
32
+ }
@@ -0,0 +1,88 @@
1
+ package org.unimodules.test.core
2
+
3
+ import expo.modules.core.Promise
4
+
5
+ enum class PromiseState {
6
+ NONE,
7
+ REJECTED,
8
+ RESOLVED,
9
+ ILLEGAL
10
+ }
11
+
12
+ class PromiseMock : Promise {
13
+
14
+ var state = PromiseState.NONE
15
+
16
+ var resolveValueSet: Boolean = false
17
+ var resolveValue: Any? = null
18
+ set(value) {
19
+ this.resolveValueSet = true
20
+ field = value
21
+ }
22
+
23
+ var rejectCodeSet: Boolean = false
24
+ var rejectCode: String? = null
25
+ set(value) {
26
+ this.rejectCodeSet = true
27
+ field = value
28
+ }
29
+
30
+ var rejectMessageSet: Boolean = false
31
+ var rejectMessage: String? = null
32
+ set(value) {
33
+ this.rejectMessageSet = true
34
+ field = value
35
+ }
36
+
37
+ var rejectThrowableSet: Boolean = false
38
+ var rejectThrowable: Throwable? = null
39
+ set(value) {
40
+ this.rejectThrowableSet = true
41
+ field = value
42
+ }
43
+
44
+ override fun resolve(value: Any?) {
45
+ assertNotResolvedNorRejected()
46
+ state = PromiseState.RESOLVED
47
+ resolveValue = value
48
+ }
49
+
50
+ override fun reject(code: String?, message: String?, e: Throwable?) {
51
+ assertNotResolvedNorRejected()
52
+ state = PromiseState.REJECTED
53
+ rejectCode = code
54
+ rejectMessage = message
55
+ rejectThrowable = e
56
+ }
57
+
58
+ override fun reject(e: Throwable?) {
59
+ assertNotResolvedNorRejected()
60
+ state = PromiseState.REJECTED
61
+ rejectThrowable = e
62
+ }
63
+
64
+ override fun reject(code: String?, message: String?) {
65
+ assertNotResolvedNorRejected()
66
+ state = PromiseState.REJECTED
67
+ rejectCode = code
68
+ rejectMessage = message
69
+ }
70
+
71
+ override fun reject(code: String?, e: Throwable?) {
72
+ assertNotResolvedNorRejected()
73
+ state = PromiseState.REJECTED
74
+ rejectCode = code
75
+ rejectThrowable = e
76
+ }
77
+
78
+ private fun assertNotResolvedNorRejected() {
79
+ when (state) {
80
+ PromiseState.RESOLVED, PromiseState.REJECTED, PromiseState.ILLEGAL -> {
81
+ state = PromiseState.ILLEGAL
82
+ throw IllegalStateException("Cannot resolve same promise twice!")
83
+ }
84
+ else -> {
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,66 @@
1
+ package org.unimodules.test.core
2
+
3
+ import android.os.Bundle
4
+ import junit.framework.ComparisonFailure
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertTrue
7
+ import expo.modules.core.arguments.MapArguments
8
+ import expo.modules.core.arguments.ReadableArguments
9
+
10
+ fun assertSetsEqual(first: Set<*>, second: Set<*>, message: String = "") {
11
+ if (!first.all { second.contains(it) }) {
12
+ throw ComparisonFailure(message, first.toString(), second.toString())
13
+ }
14
+ }
15
+
16
+ fun assertListsEqual(first: List<*>?, second: List<*>?, message: String = "") {
17
+ if (first == second) return
18
+
19
+ if (first == null || second == null) {
20
+ throw throw ComparisonFailure(message, first.toString(), second.toString())
21
+ }
22
+
23
+ if (!first.toTypedArray().contentDeepEquals(second.toTypedArray())) {
24
+ throw ComparisonFailure(message, first.toString(), second.toString())
25
+ }
26
+ }
27
+
28
+ fun assertResolved(promise: PromiseMock) {
29
+ assertEquals(PromiseState.RESOLVED, promise.state)
30
+ }
31
+
32
+ fun assertRejected(promise: PromiseMock) {
33
+ assertEquals(PromiseState.REJECTED, promise.state)
34
+ }
35
+
36
+ fun promiseResolved(promise: PromiseMock, with: (Bundle) -> Unit) {
37
+ assertResolved(promise)
38
+ with(promise.resolveValue as Bundle)
39
+ }
40
+
41
+ inline fun <reified T> promiseResolvedWithType(promise: PromiseMock, with: (T) -> Unit) {
42
+ assertResolved(promise)
43
+ assertTrue("Promise resolved with incorrect type", promise.resolveValue is T)
44
+ with(promise.resolveValue as T)
45
+ }
46
+
47
+ fun promiseRejected(promise: PromiseMock, with: (PromiseMock) -> Unit) {
48
+ assertRejected(promise)
49
+ with(promise)
50
+ }
51
+
52
+ fun assertRejectedWithCode(promise: PromiseMock, rejectCode: String) {
53
+ promiseRejected(promise) {
54
+ assertTrue("Promise has no rejection code", it.rejectCodeSet)
55
+ assertEquals(it.rejectCode, rejectCode)
56
+ }
57
+ }
58
+
59
+ fun readableArgumentsOf(values: Map<String, Any>): ReadableArguments {
60
+ return MapArguments(values)
61
+ }
62
+
63
+ fun assertStringValueNull(bundle: Bundle, key: String) {
64
+ assertTrue(bundle.containsKey(key))
65
+ assertEquals(null, bundle.getString(key))
66
+ }
@@ -0,0 +1,70 @@
1
+ package org.unimodules.test.core
2
+
3
+ import io.mockk.MockKGateway
4
+ import io.mockk.every
5
+ import io.mockk.mockk
6
+ import io.mockk.spyk
7
+ import expo.modules.core.ExportedModule
8
+ import expo.modules.core.ModuleRegistry
9
+ import expo.modules.core.interfaces.InternalModule
10
+ import java.util.*
11
+
12
+ @JvmOverloads
13
+ fun moduleRegistryMock(
14
+ internalModules: List<InternalModule> = Collections.emptyList(),
15
+ exportedModules: List<ExportedModule> = Collections.emptyList()
16
+ ): ModuleRegistry {
17
+ return mockk<ModuleRegistry>().also {
18
+ mockInternalModules(it, internalModules)
19
+ mockExternalModules(it, exportedModules)
20
+ }
21
+ }
22
+
23
+ inline fun <reified T : InternalModule> mockkInternalModule(relaxed: Boolean = false, asInterface: Class<*> = T::class.java): T {
24
+ val mock: T = mockk(relaxed = relaxed)
25
+ every { mock.exportedInterfaces } returns listOf(asInterface)
26
+ return mock
27
+ }
28
+
29
+ private fun mockInternalModules(mock: ModuleRegistry, internalModules: List<InternalModule>) {
30
+ internalModules.forEach {
31
+ mock.mockInternalModule(it)
32
+ }
33
+ every { mock.getModule<Any>(any()) } returns null
34
+ }
35
+
36
+ private fun mockExternalModules(mock: ModuleRegistry, exportedModules: List<ExportedModule>) {
37
+ exportedModules.forEach {
38
+ every { mock.getExportedModule(it.name) } returns it
39
+ }
40
+ every { mock.getExportedModule(any()) } returns null
41
+ }
42
+
43
+ fun ModuleRegistry.mockInternalModule(module: InternalModule) =
44
+ if (!this.isMock) {
45
+ throw IllegalStateException("Mocking modules available only for mocked module registry!")
46
+ } else {
47
+ module.exportedInterfaces.forEach {
48
+ every { this@mockInternalModule.getModule(it) } returns module
49
+ }
50
+ }
51
+
52
+ fun ModuleRegistry.mockExportedModule(module: ExportedModule) =
53
+ if (!this.isMock) {
54
+ throw IllegalStateException("Mocking modules available only for mocked module registry!")
55
+ } else {
56
+ every { this@mockExportedModule.getExportedModule(module.name) } returns module
57
+ }
58
+
59
+ fun mockPromise(): PromiseMock {
60
+ return spyk()
61
+ }
62
+
63
+ private val <T : Any> T.isMock: Boolean
64
+ get() {
65
+ return try {
66
+ MockKGateway.implementation().mockFactory.isMock(this)
67
+ } catch (e: UninitializedPropertyAccessException) {
68
+ false
69
+ }
70
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,46 @@
1
+ const { withProjectBuildGradle } = require('@expo/config-plugins');
2
+ const kotlinClassPath = 'org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion';
3
+
4
+ const withKotlinGradle = (config, version) => {
5
+ return withProjectBuildGradle(config, config => {
6
+ if (config.modResults.language === 'groovy') {
7
+ config.modResults.contents = setKotlinVersion(config.modResults.contents, version);
8
+ config.modResults.contents = setKotlinClassPath(config.modResults.contents);
9
+ } else {
10
+ throw new Error('Cannot setup kotlin because the build.gradle is not groovy');
11
+ }
12
+ return config;
13
+ });
14
+ };
15
+
16
+ function setKotlinVersion(buildGradle, version) {
17
+ const pattern = /kotlinVersion\s?=\s?(["'])(?:(?=(\\?))\2.)*?\1/g;
18
+ const replacement = `kotlinVersion = "${version}"`;
19
+ if (buildGradle.match(pattern)) {
20
+ // Select kotlinVersion = '***' and replace the contents between the quotes.
21
+ return buildGradle.replace(pattern, replacement);
22
+ }
23
+ return buildGradle.replace(
24
+ /ext\s?{/,
25
+ `ext {
26
+ ${replacement}`
27
+ );
28
+ }
29
+
30
+ function setKotlinClassPath(buildGradle) {
31
+ if (buildGradle.includes(kotlinClassPath)) {
32
+ return buildGradle;
33
+ }
34
+
35
+ return buildGradle.replace(
36
+ /dependencies\s?{/,
37
+ `dependencies {
38
+ classpath "${kotlinClassPath}"`
39
+ );
40
+ }
41
+
42
+ const withUnimodulesTestCore = (config) => {
43
+ return withKotlinGradle(config, '1.3.50');
44
+ }
45
+
46
+ module.exports = withUnimodulesTestCore;
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoModulesTestCore'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platform = :ios, '12.0'
14
+ s.swift_version = '5.4'
15
+ s.source = { git: 'https://github.com/expo/expo.git' }
16
+ s.static_framework = true
17
+ s.header_dir = 'ExpoModulesTestCore'
18
+
19
+ s.source_files = '**/*.{h,m,mm,swift}'
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+ s.dependency 'Quick', '~> 5.0.0'
23
+ s.dependency 'Nimble', '~> 9.2.0'
24
+ end
@@ -0,0 +1,9 @@
1
+ // Copyright 2022-present 650 Industries. All rights reserved.
2
+
3
+ #if DEBUG
4
+ @_exported import Quick
5
+ @_exported import Nimble
6
+ @_exported @testable import ExpoModulesCore
7
+
8
+ open class ExpoSpec: QuickSpec {}
9
+ #endif
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "expo-modules-test-core",
3
+ "version": "0.8.0",
4
+ "main": "./app.plugin.js",
5
+ "description": "Module providing native testing utilities for testing Expo modules",
6
+ "keywords": [
7
+ "react-native",
8
+ "expo",
9
+ "expo-modules",
10
+ "expo-modules-core",
11
+ "expo-modules-test-core"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/expo/expo.git",
16
+ "directory": "packages/expo-modules-test-core"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/expo/expo/issues"
20
+ },
21
+ "author": "650 Industries, Inc.",
22
+ "license": "MIT",
23
+ "homepage": "https://github.com/expo/expo/tree/main/packages/expo-modules-test-core",
24
+ "gitHead": "22dce752354bb429c84851bc4389abe47a766b1f"
25
+ }