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 +5 -0
- package/android/build.gradle +100 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/expo/modules/test/core/ModuleController.kt +34 -0
- package/android/src/main/java/expo/modules/test/core/ModuleMock.kt +86 -0
- package/android/src/main/java/expo/modules/test/core/ModuleMockHolder.kt +11 -0
- package/android/src/main/java/expo/modules/test/core/ModuleMockInvocationHandler.kt +104 -0
- package/android/src/main/java/expo/modules/test/core/PromiseMock.kt +71 -0
- package/android/src/main/java/expo/modules/test/core/TestJSContainerProvider.kt +12 -0
- package/android/src/main/java/expo/modules/test/core/TestUtils.kt +32 -0
- package/android/src/main/java/org/unimodules/test/core/PromiseMock.kt +88 -0
- package/android/src/main/java/org/unimodules/test/core/TestUtils.kt +66 -0
- package/android/src/main/java/org/unimodules/test/core/UnimoduleMocks.kt +70 -0
- package/app.plugin.js +46 -0
- package/ios/ExpoModulesTestCore.podspec +24 -0
- package/ios/ExpoSpec.swift +9 -0
- package/package.json +25 -0
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,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
|
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
|
+
}
|