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
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package com.segment.analytics.liveplugins.kotlin

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.segment.analytics.kotlin.core.utilities.getInt
import com.segment.analytics.kotlin.core.utilities.getString
import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences
import com.segment.analytics.substrata.kotlin.JSArray
import com.segment.analytics.substrata.kotlin.JSObject
import com.segment.analytics.substrata.kotlin.JSScope
import com.segment.analytics.substrata.kotlin.JsonElementConverter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith


@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class JSStorageTest {

private lateinit var engine: JSScope
private lateinit var jsStorage: JSStorage
private var exceptionThrown: Throwable? = null

@Before
fun setUp() {
exceptionThrown = null

engine = JSScope{ exception ->
exceptionThrown = exception
}
jsStorage = JSStorage(MemorySharedPreferences(), engine)
// Setup the engine similar to LivePlugins.configureEngine
engine.sync {
export(jsStorage, "Storage", "storage")
}
}

@Test
fun testJSStorageWithInt() {
// set from js
var value = engine.await {
evaluate("""storage.setValue("int", 1)""")
evaluate("""storage.getValue("int")""")
}
assertNull(exceptionThrown)
assertEquals(1, value)
assertEquals(1, jsStorage.getValue("int"))

// set from native
jsStorage.setValue("int", 2)
value = engine.await {
evaluate("""storage.getValue("int")""")
}
assertEquals(2, value)
assertEquals(2, jsStorage.getValue("int"))
}

@Test
fun testJSStorageWithBoolean() {
// set from js
var value = engine.await {
evaluate("""storage.setValue("boolean", true)""")
evaluate("""storage.getValue("boolean")""")
}
assertNull(exceptionThrown)
assertEquals(true, value)
assertEquals(true, jsStorage.getValue("boolean"))

// set from native
jsStorage.setValue("boolean", false)
value = engine.await {
evaluate("""storage.getValue("boolean")""")
}
assertEquals(false, value)
assertEquals(false, jsStorage.getValue("boolean"))
}

@Test
fun testJSStorageWithDouble() {
// set from js
var value = engine.await {
evaluate("""storage.setValue("double", 3.14)""")
evaluate("""storage.getValue("double")""")
}
assertNull(exceptionThrown)
assertEquals(3.14, value)
assertEquals(3.14, jsStorage.getValue("double"))

// set from native
jsStorage.setValue("double", 2.71)
value = engine.await {
evaluate("""storage.getValue("double")""")
}
assertEquals(2.71, value)
assertEquals(2.71, jsStorage.getValue("double"))
}

@Test
fun testJSStorageWithString() {
// set from js
var value = engine.await {
evaluate("""storage.setValue("string", "hello")""")
evaluate("""storage.getValue("string")""")
}
assertNull(exceptionThrown)
assertEquals("hello", value)
assertEquals("hello", jsStorage.getValue("string"))

// set from native
jsStorage.setValue("string", "world")
value = engine.await {
evaluate("""storage.getValue("string")""")
}
assertEquals("world", value)
assertEquals("world", jsStorage.getValue("string"))
}

@Test
fun testJSStorageWithLong() {
// set from js
var value = engine.await {
evaluate("""storage.setValue("long", 1234567890123)""")
evaluate("""storage.getValue("long")""")
}
assertNull(exceptionThrown)
assertEquals(1234567890123L.toDouble(), value)
assertEquals(1234567890123L.toDouble(), jsStorage.getValue("long"))

// set from native
jsStorage.setValue("long", 9876543210987L)
value = engine.await {
evaluate("""storage.getValue("long")""")
}
assertEquals(9876543210987L.toDouble(), value)
assertEquals(9876543210987L.toDouble(), jsStorage.getValue("long"))
}

@Test
fun testJSStorageWithJSObject() {
// set from js
var value = engine.await(true) {
evaluate("""storage.setValue("object", {name: "test", value: 42})""")
evaluate("""storage.getValue("object")""")
}
assertNull(exceptionThrown)
val jsonObject = JsonElementConverter.read(value).jsonObject
assertEquals("test", jsonObject.getString("name"))
assertEquals(42, jsonObject.getInt("value"))

// set from native
val nativeObject = engine.await(true) {
evaluate("""
let obj = {name: "native", value: 100}
obj
""".trimIndent())
}
jsStorage.setValue("object", nativeObject as JSObject)
value = engine.await {
evaluate("""
let obj2 = storage.getValue("object")
obj.name == obj2.name && obj.value == obj2.value
""".trimIndent())
}
assertEquals(true, value)
val jsValue = jsStorage.getValue("object")
val retrievedObject = JsonElementConverter.read(jsValue).jsonObject
assertEquals("native", retrievedObject.getString("name"))
assertEquals(100, retrievedObject.getInt("value"))
}

@Test
fun testJSStorageWithJSArray() {
// set from js
var value = engine.await(true) {
evaluate("""storage.setValue("array", [1, "test", true])""")
evaluate("""storage.getValue("array")""")
}
assertNull(exceptionThrown)
val jsonArray = JsonElementConverter.read(value).jsonArray
assertEquals(3, jsonArray.size)
assertEquals(1, jsonArray[0].jsonPrimitive.content.toInt())
assertEquals("test", jsonArray[1].jsonPrimitive.content)
assertEquals(true, jsonArray[2].jsonPrimitive.content.toBoolean())

// set from native
val nativeArray = engine.await(true) {
evaluate("""
let arr = [42, "native", false]
arr
""".trimIndent())
}
jsStorage.setValue("array", nativeArray as JSArray)
value = engine.await {
evaluate("""
let arr2 = storage.getValue("array")
arr.length == arr2.length && arr[0] == arr2[0] && arr[1] == arr2[1] && arr[2] == arr2[2]
""".trimIndent())
}
assertEquals(true, value)
val jsValue = jsStorage.getValue("array")
val retrievedArray = JsonElementConverter.read(jsValue).jsonArray
assertEquals(3, retrievedArray.size)
assertEquals(42, retrievedArray[0].jsonPrimitive.content.toInt())
assertEquals("native", retrievedArray[1].jsonPrimitive.content)
assertEquals(false, retrievedArray[2].jsonPrimitive.content.toBoolean())
}

@Test
fun testJSStorageRemoveValue() {
// 1. set from js and remove from js
engine.sync {
evaluate("""storage.setValue("jsJs", "value1")""")
evaluate("""storage.removeValue("jsJs")""")
}
assertNull(exceptionThrown)
assertNull(jsStorage.getValue("jsJs"))
val jsJsValue = engine.await(true) {
evaluate("""storage.getValue("jsJs")""")
}
assertNull(jsJsValue)

// 2. set from native and remove from native
jsStorage.setValue("nativeNative", "value2")
assertEquals("value2", jsStorage.getValue("nativeNative"))
jsStorage.removeValue("nativeNative")
assertNull(jsStorage.getValue("nativeNative"))
val nativeNativeValue = engine.await {
evaluate("""storage.getValue("nativeNative")""")
}
assertNull(nativeNativeValue)

// 3. set from js and remove from native
engine.sync {
evaluate("""storage.setValue("jsNative", "value3")""")
}
assertEquals("value3", jsStorage.getValue("jsNative"))
jsStorage.removeValue("jsNative")
assertNull(jsStorage.getValue("jsNative"))
val jsNativeValue = engine.await(true) {
evaluate("""storage.getValue("jsNative")""")
}
assertNull(jsNativeValue)

// 4. set from native and remove from js
jsStorage.setValue("nativeJs", "value4")
assertEquals("value4", jsStorage.getValue("nativeJs"))
engine.sync {
evaluate("""storage.removeValue("nativeJs")""")
}
assertNull(jsStorage.getValue("nativeJs"))
val nativeJsValue = engine.await(true) {
evaluate("""storage.getValue("nativeJs")""")
}
assertNull(nativeJsValue)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.segment.analytics.liveplugins.kotlin

import android.content.SharedPreferences
import androidx.core.content.edit
import com.segment.analytics.kotlin.core.utilities.getBoolean
import com.segment.analytics.kotlin.core.utilities.getDouble
import com.segment.analytics.kotlin.core.utilities.getInt
import com.segment.analytics.kotlin.core.utilities.getLong
import com.segment.analytics.kotlin.core.utilities.getString
import com.segment.analytics.substrata.kotlin.JSArray
import com.segment.analytics.substrata.kotlin.JSObject
import com.segment.analytics.substrata.kotlin.JSScope
import com.segment.analytics.substrata.kotlin.JsonElementConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

class JSStorage {

internal var sharedPreferences: SharedPreferences? = null

private var engine: JSScope? = null

// JSEngine requires an empty constructor to be able to export this class
constructor() {}

constructor(sharedPreferences: SharedPreferences, engine: JSScope) {
this.sharedPreferences = sharedPreferences
this.engine = engine
}

fun setValue(key: String, value: Boolean) {
save(key, value, TYPE_BOOLEAN)
}

fun setValue(key: String, value: Double) {
save(key, value, TYPE_DOUBLE)
}

fun setValue(key: String, value: Int) {
save(key, value, TYPE_INT)
}

fun setValue(key: String, value: String) {
save(key, value, TYPE_STRING)
}

fun setValue(key: String, value: Long) {
save(key, value, TYPE_LONG)
}

fun setValue(key: String, value: JSObject) {
save(
key,
JsonElementConverter.read(value),
TYPE_OBJECT
)
}

fun setValue(key: String, value: JSArray) {
save(
key,
JsonElementConverter.read(value),
TYPE_ARRAY
)
}

fun getValue(key: String): Any? {
return this.sharedPreferences?.getString(key, null)?.let {
Json.decodeFromString<JsonObject>(it).unwrap()
}
}

fun removeValue(key: String) {
this.sharedPreferences?.edit(commit = true) { remove(key) }
}

private inline fun <reified T> save(key: String, value: T, type: String) {
val jsonObject = buildJsonObject {
put(PROP_TYPE, type)
put(PROP_VALUE, Json.encodeToString(value))
}

this.sharedPreferences?.edit(commit = true) { putString(key, Json.encodeToString(jsonObject)) }
}

private fun JsonObject.unwrap(): Any? {
return when(this.getString(PROP_TYPE)) {
TYPE_BOOLEAN -> this.getBoolean(PROP_VALUE)
TYPE_INT -> this.getInt(PROP_VALUE)
TYPE_DOUBLE -> this.getDouble(PROP_VALUE)
TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString<String>(it) }
TYPE_LONG -> this.getLong(PROP_VALUE)?.toDouble()
else -> {
this.getString(PROP_VALUE)?.let {
val json = Json.decodeFromString<JsonElement>(it)
engine?.await(true) {
JsonElementConverter.write(json, context)
}
}
}
}
}

companion object {
const val PROP_TYPE = "type"
const val PROP_VALUE = "value"
const val TYPE_BOOLEAN = "boolean"
const val TYPE_INT = "int"
const val TYPE_DOUBLE = "double"
const val TYPE_STRING = "string"
const val TYPE_LONG = "long"
const val TYPE_OBJECT = "object"
const val TYPE_ARRAY = "array"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class LivePlugins(
private fun configureEngine() = engine.sync {
val jsAnalytics = JSAnalytics(analytics, engine)
export(jsAnalytics, "Analytics","analytics")
val jsStorage = JSStorage(sharedPreferences, engine)
export(jsStorage, "Storage", "storage")

evaluate(EmbeddedJS.ENUM_SETUP_SCRIPT)
evaluate(EmbeddedJS.LIVE_PLUGINS_BASE_SETUP_SCRIPT)
Expand Down