diff --git a/README.md b/README.md index 51166af..2ff2c87 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://api.bintray.com/packages/icerockdev/moko/moko-javascript/images/download.svg) ](https://bintray.com/icerockdev/moko/moko-javascript/_latestVersion) ![kotlin-version](https://img.shields.io/badge/kotlin-1.4.32-orange) +[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://img.shields.io/maven-central/v/dev.icerock.moko/javascript) ](https://repo1.maven.org/maven2/dev/icerock/moko/javascript) ![kotlin-version](https://img.shields.io/badge/kotlin-1.4.32-orange) # Mobile Kotlin javascript -This is a Kotlin MultiPlatform library that ... +This is a Kotlin MultiPlatform library that allows you to run JavaScript code from common Kotlin code ## Table of Contents - [Features](#features) @@ -14,7 +14,8 @@ This is a Kotlin MultiPlatform library that ... - [License](#license) ## Features -... +- Evaluate JavaScript code from Kotlin common code +- Pass objects to JavaScript as global vars ## Requirements - Gradle version 6.0+ @@ -43,7 +44,16 @@ dependencies { ``` ## Usage -... +```kotlin +val javaScriptEngine = JavaScriptEngine() +val result: JsType = javaScriptEngine.evaluate( + context = emptyMap(), + script = """ "Hello" + "World" """.trimIndent() +) +if (result is JsType.Str) { + println(result.value) +} +``` ## Samples More examples can be found in the [sample directory](sample). diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt index e1c91bf..d0d8bbf 100644 --- a/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/src/main/kotlin/Deps.kt @@ -8,16 +8,20 @@ object Deps { private const val androidAppCompatVersion = "1.1.0" private const val materialDesignVersion = "1.0.0" private const val androidLifecycleVersion = "2.1.0" - private const val androidCoreTestingVersion = "2.1.0" + private const val androidCoreTestingVersion = "1.3.0" + private const val testJUnitExtVersion = "1.1.2" + private const val quickjsVersion = "0.9.0" private const val coroutinesVersion = "1.4.2" - private const val mokoTestVersion = "0.2.0" + private const val kotlinxSerializationVersion = "1.1.0" + private const val mokoTestVersion = "0.3.0" + const val mokoJavascriptVersion = "0.1.0" object Android { const val compileSdk = 30 const val targetSdk = 30 - const val minSdk = 16 + const val minSdk = 18 } object Libs { @@ -25,13 +29,29 @@ object Deps { const val appCompat = "androidx.appcompat:appcompat:$androidAppCompatVersion" const val material = "com.google.android.material:material:$materialDesignVersion" const val lifecycle = "androidx.lifecycle:lifecycle-extensions:$androidLifecycleVersion" + + const val kotlinTestJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinTestVersion" + const val testRunner = "androidx.test:runner:$androidCoreTestingVersion" + const val testRules = "androidx.test:rules:$androidCoreTestingVersion" + const val testJUnitExt = "androidx.test.ext:junit:$testJUnitExtVersion" + const val testJUnitExtKtx = "androidx.test.ext:junit-ktx:$testJUnitExtVersion" + + const val quickjs = "app.cash.quickjs:quickjs-android:$quickjsVersion" } object MultiPlatform { const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + const val kotlinSerialization = + "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion" + + const val kotlinTest = + "org.jetbrains.kotlin:kotlin-test-common:$kotlinTestVersion" + const val kotlinTestAnnotations = + "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlinTestVersion" + const val mokoTest = "dev.icerock.moko:test-core:$mokoTestVersion" + const val mokoTestRobolectric = "dev.icerock.moko:test-roboelectric:$mokoTestVersion" - const val mokoTest = "dev.icerock.moko:test:$mokoTestVersion" const val mokoJavascript = "dev.icerock.moko:javascript:$mokoJavascriptVersion" } } diff --git a/buildSrc/src/main/kotlin/publication-convention.gradle.kts b/buildSrc/src/main/kotlin/publication-convention.gradle.kts index 0ccf20c..51f0985 100644 --- a/buildSrc/src/main/kotlin/publication-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/publication-convention.gradle.kts @@ -1,17 +1,79 @@ +import java.util.Base64 + plugins { id("org.gradle.maven-publish") + id("signing") } -publishing { - group = "dev.icerock.moko" - version = Deps.mokoJavascriptVersion +group = "dev.icerock.moko" +version = Deps.mokoJavascriptVersion + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} - repositories.maven("https://api.bintray.com/maven/icerockdev/moko/moko-javascript/;publish=1") { - name = "bintray" +publishing { + repositories.maven("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") { + name = "OSSRH" credentials { - username = System.getenv("BINTRAY_USER") - password = System.getenv("BINTRAY_KEY") + username = System.getenv("OSSRH_USER") + password = System.getenv("OSSRH_KEY") + } + } + + publications.withType { + // Stub javadoc.jar artifact + artifact(javadocJar.get()) + + // Provide artifacts information requited by Maven Central + pom { + name.set("MOKO JavaScript") + description.set("JavaScript code evaluation from common code for Kotlin Multiplatform Mobile") + url.set("https://github.com/icerockdev/moko-javascript") + licenses { + license { + name.set("Apache-2.0") + distribution.set("repo") + url.set("https://github.com/icerockdev/moko-javascript/blob/master/LICENSE.md") + } + } + + developers { + developer { + id.set("Tetraquark") + name.set("Vladislav Areshkin") + email.set("vareshkin@icerockdev.com") + } + developer { + id.set("Dorofeev") + name.set("Andrey Dorofeev") + email.set("adorofeev@icerockdev.com") + } + developer { + id.set("Alex009") + name.set("Aleksey Mikhailov") + email.set("aleksey.mikhailov@icerockdev.com") + } + } + + scm { + connection.set("scm:git:ssh://github.com/icerockdev/moko-javascript.git") + developerConnection.set("scm:git:ssh://github.com/icerockdev/moko-javascript.git") + url.set("https://github.com/icerockdev/moko-javascript") + } + } + } + + signing { + val signingKeyId: String? = System.getenv("SIGNING_KEY_ID") + val signingPassword: String? = System.getenv("SIGNING_PASSWORD") + val signingKey: String? = System.getenv("SIGNING_KEY")?.let { base64Key -> + String(Base64.getDecoder().decode(base64Key)) + } + if (signingKeyId != null) { + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications) } } } diff --git a/javascript/build.gradle.kts b/javascript/build.gradle.kts index 928d042..eb26148 100644 --- a/javascript/build.gradle.kts +++ b/javascript/build.gradle.kts @@ -7,6 +7,53 @@ plugins { id("publication-convention") } +android { + testOptions.unitTests.isIncludeAndroidResources = true + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + packagingOptions { + exclude("META-INF/*.kotlin_module") + exclude("META-INF/*.kotlin_module") + exclude("META-INF/AL2.0") + exclude("META-INF/LGPL2.1") + } + + sourceSets { + getByName("androidTest").java.srcDirs( + file("src/androidAndroidTest/kotlin"), + file("src/mobileDeviceTest/kotlin") + ) + } +} + +kotlin { + sourceSets { + val mobileDeviceTest by creating + + val commonTest by getting + val iosTest by getting + val androidAndroidTest by getting + + mobileDeviceTest.dependsOn(commonTest) + iosTest.dependsOn(mobileDeviceTest) + androidAndroidTest.dependsOn(mobileDeviceTest) + } +} + dependencies { + androidMainImplementation(Deps.Libs.Android.quickjs) + + commonMainImplementation(Deps.Libs.MultiPlatform.kotlinSerialization) + + commonTestImplementation(Deps.Libs.MultiPlatform.kotlinTest) + commonTestImplementation(Deps.Libs.MultiPlatform.kotlinTestAnnotations) commonTestImplementation(Deps.Libs.MultiPlatform.mokoTest) + + androidTestImplementation(Deps.Libs.Android.kotlinTestJUnit) + androidTestImplementation(Deps.Libs.Android.testRunner) + androidTestImplementation(Deps.Libs.Android.testRules) + androidTestImplementation(Deps.Libs.Android.testJUnitExt) + androidTestImplementation(Deps.Libs.Android.testJUnitExtKtx) } diff --git a/javascript/src/androidMain/AndroidManifest.xml b/javascript/src/androidMain/AndroidManifest.xml index 0da09ab..309ebc8 100644 --- a/javascript/src/androidMain/AndroidManifest.xml +++ b/javascript/src/androidMain/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/javascript/src/androidMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt b/javascript/src/androidMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt new file mode 100644 index 0000000..07917c3 --- /dev/null +++ b/javascript/src/androidMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import app.cash.quickjs.QuickJs +import app.cash.quickjs.QuickJsException +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +actual class JavaScriptEngine actual constructor() { + private val quickJs: QuickJs = QuickJs.create() + private val json: Json = Json.Default + + @Volatile + var isClosed = false + private set + + actual fun evaluate( + context: Map, + script: String + ): JsType { + if (isClosed) throw JavaScriptEvaluationException(message = "Engine already closed") + + return try { + internalEvaluate(context, script) + } catch (exception: QuickJsException) { + throw JavaScriptEvaluationException(exception, exception.message) + } + } + + actual fun close() { + if (isClosed) return + quickJs.close() + isClosed = true + } + + private fun internalEvaluate( + context: Map, + script: String + ): JsType { + val scriptWithContext = convertContextMapToJsScript(context) + script + "\n" + val result = quickJs.evaluate(scriptWithContext) + return handleQuickJsResult(result) + } + + // TODO fix pass of arguments - now wrapping of string and json invalid and will be broken on multilined strings + private fun convertContextMapToJsScript(context: Map): String { + if (context.isEmpty()) return "" + + return context.mapNotNull { pair -> + prepareValueForJs(pair.value)?.let { "var ${pair.key} = $it;" } + }.joinToString(separator = "") + } + + private fun prepareValueForJs(valueWrapper: JsType): String? { + return when (valueWrapper) { + is JsType.Bool -> valueWrapper.value.toString() + is JsType.DoubleNum -> valueWrapper.value.toString() + is JsType.Json -> valueWrapper.value.let { + Json.encodeToString(JsonElement.serializer(), it) + }.let { + it.replace("\"", "\\\"") + }.let { + "JSON.parse(\"$it\")" + } + is JsType.Str -> valueWrapper.value.let { + it.replace("\"", "\\\"") + }.let { + "\"$it\"" + } + JsType.Null -> null + } + } + + private fun handleQuickJsResult(result: Any?): JsType { + return when (result) { + null -> JsType.Null + is Boolean -> JsType.Bool(result) + is Int -> JsType.DoubleNum(result.toDouble()) + is Double -> JsType.DoubleNum(result) + is Float -> JsType.DoubleNum(result.toDouble()) + is String -> try { + val serializeResult = json.parseToJsonElement(result) + if (serializeResult is JsonObject) { + JsType.Json(serializeResult) + } else { + JsType.Str(result) + } + } catch (ex: SerializationException) { + JsType.Str(result) + } catch (ex: IllegalStateException) { + JsType.Str(result) + } + else -> throw JavaScriptEvaluationException( + message = "Impossible JavaScriptEngine handler state with result [$result]" + ) + } + } +} diff --git a/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt index ef51a05..fd41fa2 100644 --- a/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt +++ b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt @@ -5,5 +5,16 @@ package dev.icerock.moko.javascript expect class JavaScriptEngine() { + /** + * Evaluate some [script] with external [context]. + * + * @throws JavaScriptEvaluationException in case of an error in the engine evaluation or if the + * engine has already been closed. + */ fun evaluate(context: Map, script: String): JsType + + /** + * Closes the engine and releases the allocated memory. + */ + fun close() } diff --git a/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEvaluationException.kt b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEvaluationException.kt new file mode 100644 index 0000000..56ed7a5 --- /dev/null +++ b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JavaScriptEvaluationException.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +class JavaScriptEvaluationException( + cause: Throwable? = null, + message: String? = null +) : Exception(message, cause) diff --git a/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JsType.kt b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JsType.kt index c367b93..f711811 100644 --- a/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JsType.kt +++ b/javascript/src/commonMain/kotlin/dev/icerock/moko/javascript/JsType.kt @@ -4,10 +4,26 @@ package dev.icerock.moko.javascript +import kotlinx.serialization.json.JsonElement + sealed class JsType { - data class Boolean(val value: Boolean): JsType - data class String(val value: String): JsType - data class Int(val value: Int): JsType - data class Double(val value: Double): JsType - data class Json(val value: JsonElement): JsType + data class Bool(override val value: Boolean) : JsType() + data class Str(override val value: String) : JsType() + data class DoubleNum(override val value: Double) : JsType() + data class Json(override val value: JsonElement) : JsType() + + /** + * For "undefined" and "null". + */ + object Null : JsType() { + override val value: Any? get() = null + } + + abstract val value: Any? } + +fun JsType.boolValue(): Boolean = (this as JsType.Bool).value +fun JsType.stringValue(): String = (this as JsType.Str).value +fun JsType.doubleValue(): Double = (this as JsType.DoubleNum).value +fun JsType.jsonValue(): JsonElement = (this as JsType.Json).value +fun JsType.nullValue(): Any? = (this as JsType.Null).let { null } diff --git a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt b/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt deleted file mode 100644 index 0309b38..0000000 --- a/javascript/src/commonTest/kotlin/dev/icerock/moko/javascript/SampleTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.javascript - -import kotlin.test.Test -import kotlin.test.assertEquals - -class SampleTest { - @Test - fun `equality test`() { - assertEquals(expected = 4, actual = multiply(a = 2, b = 2)) - } -} diff --git a/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt b/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt new file mode 100644 index 0000000..61bce11 --- /dev/null +++ b/javascript/src/iosMain/kotlin/dev/icerock/moko/javascript/JavaScriptEngine.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.encodeToJsonElement +import platform.Foundation.NSArray +import platform.Foundation.NSDictionary +import platform.Foundation.NSJSONSerialization +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding +import platform.JavaScriptCore.JSContext +import platform.JavaScriptCore.JSValue +import platform.JavaScriptCore.setObject + +actual class JavaScriptEngine actual constructor() { + actual fun evaluate(context: Map, script: String): JsType { + + val jsContext = JSContext() + + jsContext.exceptionHandler = { exceptionContext, exception -> + val message = "\"context = $exceptionContext, exception = $exception\"" + throw JavaScriptEvaluationException(cause = null, message = message) + } + + context.forEach { + jsContext.setObject( + `object` = prepareValueForJsContext(it.value), + forKeyedSubscript = NSString.create(string = it.key) + ) + } + + val result = jsContext.evaluateScript(script) + + return result?.toMokoJSType() ?: JsType.Null + } + + actual fun close() { + // Nothing to do here + } + + private fun prepareValueForJsContext(valueWrapper: JsType): Any? { + return if (valueWrapper is JsType.Json) valueWrapper.value.getValue() + else valueWrapper.value + } +} + +private fun JsonObject.toNSDictionary(): NSDictionary { + val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding) + ?: return NSDictionary() + return (NSJSONSerialization.JSONObjectWithData( + data = data, + options = 0, + error = null + ) as? NSDictionary) ?: NSDictionary() +} + +private fun JsonArray.toNSArray(): NSArray { + val data = NSString.create(string = this.toString()).dataUsingEncoding(NSUTF8StringEncoding) + ?: return NSArray() + return (NSJSONSerialization.JSONObjectWithData( + data = data, + options = 0, + error = null + ) as? NSArray) ?: NSArray() +} + +private fun JsonElement.getValue(): Any? { + return (this as? JsonObject)?.toNSDictionary() + ?: (this as? JsonArray)?.toNSArray() + ?: (this as? JsonPrimitive)?.content +} + +private fun JSValue.toMokoJSType(): JsType { + return when { + isBoolean -> JsType.Bool(toBool()) + isString -> JsType.Str(toString_().orEmpty()) + isNumber -> JsType.DoubleNum(toDouble()) + isObject -> JsType.Json(Json.encodeToJsonElement(toDictionary())) + isArray -> JsType.Json(Json.encodeToJsonElement(toArray())) + isUndefined -> JsType.Null + isNull -> JsType.Null + else -> JsType.Null + } +} diff --git a/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineCrossTypesTests.kt b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineCrossTypesTests.kt new file mode 100644 index 0000000..bba83f5 --- /dev/null +++ b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineCrossTypesTests.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavaScriptEngineCrossTypesTests { + private lateinit var javaScriptEngine: JavaScriptEngine + private lateinit var context: Map + + @BeforeTest + fun init() { + javaScriptEngine = JavaScriptEngine() + + val list = listOf(5, 15) + val listJson = Json.encodeToJsonElement(list) + + context = mapOf( + "list" to JsType.Json(listJson), + "number" to JsType.DoubleNum(4.0), + "doubleString" to JsType.Str(" Hello ") + ) + } + + @AfterTest + fun dispose() { + javaScriptEngine.close() + } + + @Test + fun numberWithNumberSumCheck() { + assertEquals( + expected = JsType.DoubleNum(19.0), + actual = javaScriptEngine.evaluate(context = context, script = "list[1]+number") + ) + } + + @Test + fun stringWithNumberSumCheck() { + assertEquals( + actual = JsType.Str(" Hello 5"), + expected = javaScriptEngine.evaluate(context = context, script = "doubleString+list[0]") + ) + } +} diff --git a/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineJsonTests.kt b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineJsonTests.kt new file mode 100644 index 0000000..16e210d --- /dev/null +++ b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineJsonTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavaScriptEngineJsonTests { + private lateinit var javaScriptEngine: JavaScriptEngine + private lateinit var context: Map + + @BeforeTest + fun init() { + javaScriptEngine = JavaScriptEngine() + + val form = mapOf("selector_1" to "first_value", "selector_2" to "second_value") + val profile = mapOf("email" to "test@test.com") + val formJson = Json.encodeToJsonElement(form) + val profileJson = Json.encodeToJsonElement(profile) + + context = mapOf( + "form" to JsType.Json(formJson), + "profile" to JsType.Json(profileJson) + ) + } + + @AfterTest + fun dispose() { + javaScriptEngine.close() + } + + @Test + fun formSelectorCheck() { + assertEquals( + expected = JsType.Bool(true), + actual = javaScriptEngine.evaluate( + context = context, + script = "form.selector_1 == \"first_value\"" + ) + ) + } + + @Test + fun profileEmailCheck() { + assertEquals( + expected = JsType.Bool(true), + actual = javaScriptEngine.evaluate(context = context, script = "profile.email != null") + ) + } + + @Test + fun profileStringGet() { + assertEquals( + expected = JsType.Str("test@test.com"), + actual = javaScriptEngine.evaluate(context = context, script = "profile.email") + ) + } + + @Test + fun profileNullGet() { + assertEquals( + expected = JsType.Null, + actual = javaScriptEngine.evaluate(context = context, script = "profile.first_name") + ) + } +} diff --git a/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineSimpleTypesTests.kt b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineSimpleTypesTests.kt new file mode 100644 index 0000000..ae27b4f --- /dev/null +++ b/javascript/src/mobileDeviceTest/kotlin/dev/icerock/moko/javascript/JavaScriptEngineSimpleTypesTests.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.javascript + +import kotlinx.serialization.json.JsonObject +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class JavaScriptEngineSimpleTypesTests { + private lateinit var javaScriptEngine: JavaScriptEngine + + @BeforeTest + fun init() { + javaScriptEngine = JavaScriptEngine() + } + + @AfterTest + fun dispose() { + javaScriptEngine.close() + } + + @Test + fun integerSumCheck() { + assertEquals( + expected = 101.0, + actual = javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var a = 1 + 100; + a + """.trimIndent() + ).doubleValue() + ) + } + + @Test + fun doubleSumCheck() { + assertEquals( + expected = 1.5, + actual = javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var a = 0.5 + 1.0; + a + """.trimIndent() + ).doubleValue() + ) + } + + @Test + fun stringReturnCheck() { + assertEquals( + expected = "string", + actual = javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var a = "string"; + a + """.trimIndent() + ).stringValue() + ) + } + + @Test + fun booleanCheck() { + assertEquals( + expected = false, + actual = javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var a = false == true; + a + """.trimIndent() + ).boolValue() + ) + } + + @Ignore // on iOS we got StrValue, on Android - JsonValue + @Test + fun jsonReadCheck() { + assertTrue { + javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var obj = {male:"Ford", model:"Mustang", number:10}; + JSON.stringify(obj); + """.trimIndent() + ).jsonValue() is JsonObject + } + } + + @Test + fun undefinedCheck() { + assertNull( + javaScriptEngine.evaluate( + context = emptyMap(), + script = """ + var a = undefined + a + """.trimIndent() + ).nullValue() + ) + } +} diff --git a/sample/android-app/src/main/AndroidManifest.xml b/sample/android-app/src/main/AndroidManifest.xml index 1485409..adf12eb 100644 --- a/sample/android-app/src/main/AndroidManifest.xml +++ b/sample/android-app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ diff --git a/sample/ios-app/ios-app.xcodeproj/project.pbxproj b/sample/ios-app/ios-app.xcodeproj/project.pbxproj index fee4d55..f6169bb 100644 --- a/sample/ios-app/ios-app.xcodeproj/project.pbxproj +++ b/sample/ios-app/ios-app.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ hasScannedForEncodings = 0; knownRegions = ( English, + Base, ); mainGroup = 287627F61F319065007FA12B; productRefGroup = 287628001F319065007FA12B /* Products */; @@ -270,6 +271,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4VU932NX78; INFOPLIST_FILE = src/Info.plist; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript; PRODUCT_NAME = mokoSampleJavascript; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -288,6 +290,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4VU932NX78; INFOPLIST_FILE = src/Info.plist; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.javascript; PRODUCT_NAME = mokoSampleJavascript; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/sample/ios-app/src/Resources/Base.lproj/Main.storyboard b/sample/ios-app/src/Resources/Base.lproj/Main.storyboard index 594bc2b..ca8b011 100644 --- a/sample/ios-app/src/Resources/Base.lproj/Main.storyboard +++ b/sample/ios-app/src/Resources/Base.lproj/Main.storyboard @@ -1,11 +1,8 @@ - - - - + + - - + @@ -14,7 +11,7 @@ - + @@ -28,7 +25,7 @@ - + @@ -36,8 +33,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/ios-app/src/TestViewController.swift b/sample/ios-app/src/TestViewController.swift index 23a9c17..895ec43 100644 --- a/sample/ios-app/src/TestViewController.swift +++ b/sample/ios-app/src/TestViewController.swift @@ -7,7 +7,22 @@ import MultiPlatformLibrary class TestViewController: UIViewController { + @IBOutlet private var firstValueTextField: UITextField! + @IBOutlet private var secondValueTextField: UITextField! + @IBOutlet private var resultLabel: UILabel! + override func viewDidLoad() { super.viewDidLoad() } + + @IBAction private func run() { + let result = Calculator().run(a: firstValueTextField.text ?? "", b: secondValueTextField.text ?? "") + if let value = (result as? JsType.Str)?.value { + resultLabel.text = value + } + + if let value = (result as? JsType.DoubleNum)?.value { + resultLabel.text = "\(value)" + } + } } diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 4c3ed48..04ed8b3 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { commonMainApi(Deps.Libs.MultiPlatform.coroutines) + commonMainApi(Deps.Libs.MultiPlatform.kotlinSerialization) commonMainApi(Deps.Libs.MultiPlatform.mokoJavascript) } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt index 4489719..c377903 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/Calculator.kt @@ -4,10 +4,32 @@ package com.icerockdev.library -import dev.icerock.moko.javascript.multiply +import dev.icerock.moko.javascript.JavaScriptEngine +import dev.icerock.moko.javascript.JsType class Calculator { - fun run() { - println(multiply(a = 2, b = 3)) + fun run(a: String, b: String): JsType { + val engine = JavaScriptEngine() + val testScript = "a+b" + + val numberA = a.toDoubleOrNull() + val numberB = b.toDoubleOrNull() + + val context = if (numberA != null && numberB != null) { + mapOf( + "a" to JsType.DoubleNum(numberA), + "b" to JsType.DoubleNum(numberB) + ) + } else { + mapOf( + "a" to JsType.Str(a), + "b" to JsType.Str(b) + ) + } + + return engine.evaluate( + context = context, + script = testScript + ) } }