diff --git a/readme.md b/readme.md index 7cb3d8a..0fd1a50 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ The Nimbus SDUI is: 1. A solution for applications that need to have some of its user interface (UI) driven by the backend, i.e. Server Driven UI (SDUI). -1. A protocol for serializing the content and behavior of a UI into JSON so it can be sent by a backend sever and interpreted by the front-end. +1. A protocol for serializing the content and behavior of a UI into JSON so it can be sent by a backend server and interpreted by the front-end. 1. A set of libraries that implements this protocol. An application that uses Nimbus will have: diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ServerDrivenView.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ServerDrivenView.kt index dfb7520..e8f765d 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/ServerDrivenView.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ServerDrivenView.kt @@ -17,6 +17,7 @@ package br.com.zup.nimbus.core import br.com.zup.nimbus.core.scope.CommonScope +import br.com.zup.nimbus.core.tree.ServerDrivenEvent /** * A scope for the current view in a navigator. @@ -30,6 +31,7 @@ class ServerDrivenView( * The states in this scope. Useful for creating view parameters in the navigation. */ states: List? = null, + val events: List? = null, /** * A description for this view. Suggestion: the URL used to load the content of this view or "json", if a local json * string was used to load it. @@ -44,7 +46,7 @@ class ServerDrivenView( getNavigator: () -> ServerDrivenNavigator, ): CommonScope(parent = nimbus, states = states) { constructor(nimbus: Nimbus, getNavigator: () -> ServerDrivenNavigator): - this(nimbus, null, null, getNavigator) + this(nimbus, null, null, null, getNavigator) val navigator = getNavigator() } diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/network/ViewRequest.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/network/ViewRequest.kt index bb4894f..3ab51f4 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/network/ViewRequest.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/network/ViewRequest.kt @@ -16,6 +16,8 @@ package br.com.zup.nimbus.core.network +import br.com.zup.nimbus.core.tree.ServerDrivenEvent + data class ViewRequest( /** * The URL to send the request to. When it starts with "/", it's relative to the BaseUrl. @@ -38,7 +40,8 @@ data class ViewRequest( */ val fallback: Map? = null, /** - * The map of state ids and its values that will be used on the next page. + * The map of states and their values that will be used on the next page. */ - val params: Map? = null, + val state: Map? = null, + val events: List? = null, ) diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/tree/dynamic/node/DynamicNode.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/tree/dynamic/node/DynamicNode.kt index 599362d..76dc545 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/tree/dynamic/node/DynamicNode.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/tree/dynamic/node/DynamicNode.kt @@ -16,13 +16,17 @@ package br.com.zup.nimbus.core.tree.dynamic.node +import br.com.zup.nimbus.core.Nimbus import br.com.zup.nimbus.core.scope.CloneAfterInitializationError import br.com.zup.nimbus.core.scope.DoubleInitializationError import br.com.zup.nimbus.core.ServerDrivenState import br.com.zup.nimbus.core.dependency.CommonDependency +import br.com.zup.nimbus.core.dependency.Dependency +import br.com.zup.nimbus.core.dependency.Dependent import br.com.zup.nimbus.core.scope.CommonScope import br.com.zup.nimbus.core.scope.LazilyScoped import br.com.zup.nimbus.core.scope.Scope +import br.com.zup.nimbus.core.scope.closestScopeWithType import br.com.zup.nimbus.core.tree.ServerDrivenNode import br.com.zup.nimbus.core.tree.dynamic.container.NodeContainer import br.com.zup.nimbus.core.tree.dynamic.container.PropertyContainer @@ -67,9 +71,28 @@ open class DynamicNode( hasChanged = true } + /** + * Compute the values of states that have been provided expressions as their initial values. + */ + private fun initializeDependentStates() { + if (states?.isEmpty() != false) return + val expressionParser = closestScopeWithType()?.expressionParser ?: return + + states?.forEach { state -> + val value = state.get() + if (value is String && expressionParser.containsExpression(value)) { + val parsed = expressionParser.parseString(value, true) + if (parsed is LazilyScoped<*>) parsed.initialize(this) + if (parsed is Dependent) parsed.update() + state.setSilently(parsed.getValue()) + } + } + } + override fun initialize(scope: Scope) { if (parent != null) throw DoubleInitializationError() parent = scope + initializeDependentStates() propertyContainer?.initialize(this) childrenContainer?.initialize(this) propertyContainer?.addDependent(this) diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/navigation.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/navigation.kt index 3c52668..a267644 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/navigation.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/navigation.kt @@ -22,7 +22,6 @@ import br.com.zup.nimbus.core.network.ServerDrivenHttpMethod import br.com.zup.nimbus.core.network.ViewRequest import br.com.zup.nimbus.core.ActionTriggeredEvent import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData -import br.com.zup.nimbus.core.deserialization.SerializationError import br.com.zup.nimbus.core.ui.action.error.ActionExecutionError import br.com.zup.nimbus.core.ui.action.error.ActionDeserializationError @@ -35,10 +34,12 @@ private fun requestFromEvent(event: ActionEvent, isPushOrPresent: Boolean): View val headers = properties.get("headers").asMapOrNull()?.mapValues { it.value.asString() } val body = attemptJsonSerialization(properties.get("body"), event) val fallback = properties.get("fallback").asMapOrNull()?.mapValues { it.value.asAnyOrNull() } - val params = if (isPushOrPresent) properties.get("params").asMapOrNull()?.mapValues { it.value.asAnyOrNull() } + val state = if (isPushOrPresent) properties.get("state").asMapOrNull()?.mapValues { it.value.asAnyOrNull() } + else null + val events = if (isPushOrPresent) properties.get("events").asMapOrNull()?.map { it.value.asEvent() } else null if (properties.hasError()) throw ActionDeserializationError(event, properties) - return ViewRequest(url, method, headers, body, fallback, params) + return ViewRequest(url, method, headers, body, fallback, state, events) } private fun pushOrPresent(event: ActionTriggeredEvent, isPush: Boolean) { diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/triggerViewEvent.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/triggerViewEvent.kt new file mode 100644 index 0000000..3d618ed --- /dev/null +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/action/triggerViewEvent.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.ui.action + +import br.com.zup.nimbus.core.ActionTriggeredEvent +import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData +import br.com.zup.nimbus.core.ui.action.error.ActionDeserializationError + +internal fun triggerViewEvent(event: ActionTriggeredEvent) { + val data = AnyServerDrivenData(event.action.properties) + val nameOfEventToTrigger = data.get("event").asString() + val valueForEventToTrigger = data.get("value").asAnyOrNull() + if (data.hasError()) { + throw ActionDeserializationError(event, data) + } + val eventToTrigger = event.scope.view.events?.find { it.name == nameOfEventToTrigger } + if (eventToTrigger == null) { + event.scope.nimbus.logger.error("Can't trigger view event named \"$nameOfEventToTrigger\" because the current " + + "view has no such event.") + } else { + eventToTrigger.run(valueForEventToTrigger) + } +} diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/coreUILibrary.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/coreUILibrary.kt index 2125e80..23a52f5 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/coreUILibrary.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/coreUILibrary.kt @@ -26,9 +26,11 @@ import br.com.zup.nimbus.core.ui.action.present import br.com.zup.nimbus.core.ui.action.push import br.com.zup.nimbus.core.ui.action.sendRequest import br.com.zup.nimbus.core.ui.action.setState +import br.com.zup.nimbus.core.ui.action.triggerViewEvent import br.com.zup.nimbus.core.ui.operations.registerArrayOperations import br.com.zup.nimbus.core.ui.operations.registerLogicOperations import br.com.zup.nimbus.core.ui.operations.registerNumberOperations +import br.com.zup.nimbus.core.ui.operations.registerObjectOperations import br.com.zup.nimbus.core.ui.operations.registerOtherOperations import br.com.zup.nimbus.core.ui.operations.registerStringOperations @@ -42,6 +44,7 @@ val coreUILibrary = UILibrary("") .addAction("popTo") { popTo(it) } .addAction("present") { present(it) } .addAction("dismiss") { dismiss(it) } + .addAction("triggerViewEvent") { triggerViewEvent(it) } .addAction("log") { log(it) } .addAction("sendRequest") { sendRequest(it) } .addAction("setState") { setState(it) } @@ -56,5 +59,6 @@ val coreUILibrary = UILibrary("") registerNumberOperations(this) registerOtherOperations(this) registerStringOperations(this) + registerObjectOperations(this) this } diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/array.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/array.kt index 9d43832..787e1db 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/array.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/array.kt @@ -21,6 +21,7 @@ import br.com.zup.nimbus.core.ui.UILibrary internal fun registerArrayOperations(library: UILibrary) { library + .addOperation("array") { it } .addOperation("insert") { val arguments = AnyServerDrivenData(it) val list = if (arguments.at(0).isList()) (arguments.at(0).value as List<*>).toMutableList() diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/object.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/object.kt new file mode 100644 index 0000000..a5f5426 --- /dev/null +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/object.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.ui.operations + +import br.com.zup.nimbus.core.ui.UILibrary + +internal fun registerObjectOperations(library: UILibrary) { + library + .addOperation("object") { + val objectMap = mutableMapOf() + for (i in it.indices step 2) { + objectMap[it.getOrNull(i).toString()] = it.getOrNull(i + 1) + } + objectMap + } + .addOperation("entries") { + val result = it.firstOrNull()?.let { map -> + if (map is Map<*, *>) map.entries.map { entry -> mapOf("key" to entry.key, "value" to entry.value) } + else null + } + result ?: emptyList() + } +} diff --git a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/other.kt b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/other.kt index f53bce4..cde9a6c 100644 --- a/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/other.kt +++ b/src/commonMain/kotlin/br/com/zup/nimbus/core/ui/operations/other.kt @@ -16,7 +16,6 @@ package br.com.zup.nimbus.core.ui.operations -import br.com.zup.nimbus.core.deserialization.AnyServerDrivenData import br.com.zup.nimbus.core.ui.UILibrary import br.com.zup.nimbus.core.utils.Null import br.com.zup.nimbus.core.utils.compareTo @@ -28,7 +27,7 @@ private fun areNumbersEqual(left: Any?, right: Any?): Boolean { return leftNumber.compareTo(rightNumber) == 0 } -@Suppress("ComplexMethod", "LongMethod") +@Suppress("ComplexMethod") internal fun registerOtherOperations(library: UILibrary) { library .addOperation("contains"){ @@ -85,11 +84,4 @@ internal fun registerOtherOperations(library: UILibrary) { else -> Null.isNull(collection) } } - .addOperation("entries"){ - val result = it.firstOrNull()?.let { map -> - if (map is Map<*, *>) map.entries.map { entry -> mapOf("key" to entry.key, "value" to entry.value) } - else null - } - result ?: emptyList() - } } diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/ObservableNavigator.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/ObservableNavigator.kt index f567939..35da8fb 100644 --- a/src/commonTest/kotlin/br/com/zup/nimbus/core/ObservableNavigator.kt +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/ObservableNavigator.kt @@ -41,8 +41,8 @@ class ObservableNavigator( } override fun push(request: ViewRequest) { - val states = request.params?.map { ServerDrivenState(it.key, it.value) } - val view = ServerDrivenView(nimbus, states = states, description = request.url) { this } + val states = request.state?.map { ServerDrivenState(it.key, it.value) } + val view = ServerDrivenView(nimbus, states = states, events = request.events, description = request.url) { this } testScope.launch { try { val tree = nimbus.viewClient.fetch(request) diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/NavigationCallbackTest.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/NavigationCallbackTest.kt new file mode 100644 index 0000000..25d246d --- /dev/null +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/NavigationCallbackTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.integration.navigation + +import br.com.zup.nimbus.core.Nimbus +import br.com.zup.nimbus.core.NodeUtils +import br.com.zup.nimbus.core.ObservableNavigator +import br.com.zup.nimbus.core.ServerDrivenConfig +import br.com.zup.nimbus.core.network.DefaultHttpClient +import br.com.zup.nimbus.core.network.ViewRequest +import br.com.zup.nimbus.core.scope.closestState +import br.com.zup.nimbus.core.tree.ServerDrivenNode +import br.com.zup.nimbus.core.tree.findNodeById +import br.com.zup.nimbus.core.ui.UILibrary +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class NavigationCallbackTest { + private val scope = TestScope() + private var savedNote: Any? = null + private val ui = UILibrary("test").addAction("saveNote") { + savedNote = it.action.properties?.get("note") + } + private val nimbus = Nimbus( + ServerDrivenConfig( + baseUrl = BASE_URL, + platform = "test", + httpClient = DefaultHttpClient(callbackServerMock), + ui = listOf(ui) + ) + ) + private val navigator = ObservableNavigator(scope, nimbus) + private val noteId = 1 + private val noteTitle = "My first note" + private val noteDescription = "Description of my first note" + private val newTitle = "My edited note" + private val newDescription = "Description of my edited note" + + @BeforeTest + fun clear() { + navigator.clear() + savedNote = null + } + + private suspend fun listNotesAndEditFirst(): ServerDrivenNode { + // WHEN we load the screen that lists all notes + navigator.push(ViewRequest("/list")) + val listScreen = navigator.awaitPushCompletion() + // AND we click the first note in order to edit it + NodeUtils.pressButton(listScreen, "edit:1") + // AND we wait for the edition screen to load + val editScreen = navigator.awaitPushCompletion() + //NodeUtils.triggerEvent(editScreen.findNodeById("form"), "onInit") + // THEN the form should be pre-filled with the values of the first note + val titleInput = editScreen.findNodeById("title") + val descriptionInput = editScreen.findNodeById("description") + assertEquals(noteTitle, titleInput?.properties?.get("value")) + assertEquals(noteDescription, descriptionInput?.properties?.get("value")) + // WHEN we edit each text field + NodeUtils.triggerEvent(titleInput, "onChange", newTitle) + NodeUtils.triggerEvent(descriptionInput, "onChange", newDescription) + return editScreen + } + + @Test + fun shouldSaveEditedNoteOnSecondPage() = scope.runTest { + // WHEN we list all notes, navigate to edit page of the first note and change the values in the form + val editionScreen = listNotesAndEditFirst() + // AND press "save" + NodeUtils.pressButton(editionScreen, "save") + // THEN it should be showing the list screen + val currentScreen = navigator.pages.last() + assertTrue(currentScreen.findNodeById("edit:1") != null) + assertTrue(currentScreen.findNodeById("edit:2") != null) + // AND the callback of the first page (saveNote) should have been called with the edited note + assertEquals( + mapOf("id" to noteId, "title" to newTitle, "description" to newDescription), + savedNote, + ) + // AND the textual (ui) representation of the first note should have been updated + assertEquals("$newTitle: $newDescription", currentScreen.findNodeById("text:1")?.properties?.get("text")) + // AND the first note should have been updated (actual list state) + val notes = currentScreen.children?.first()?.closestState("notes") + assertEquals( + mapOf("id" to noteId, "title" to newTitle, "description" to newDescription), + (notes?.get() as List).firstOrNull(), + ) + } + + @Test + fun shouldCancelEditedNoteOnSecondPage() = scope.runTest { + // WHEN we list all notes, navigate to edit page of the first note and change the values in the form + val editionScreen = listNotesAndEditFirst() + // AND press "cancel" + NodeUtils.pressButton(editionScreen, "cancel") + // THEN the list screen should be showing + val currentScreen = navigator.pages.last() + assertTrue(currentScreen.findNodeById("edit:1") != null) + assertTrue(currentScreen.findNodeById("edit:2") != null) + // AND the textual (ui) representation of the first note should not have changed + assertEquals("$noteTitle: $noteDescription", currentScreen.findNodeById("text:1")?.properties?.get("text")) + // AND the value of the first item in the forEach should not have changed (for each item state) + val item = currentScreen.findNodeById("edit:1")?.closestState("item") + assertEquals( + mapOf("id" to noteId, "title" to noteTitle, "description" to noteDescription), + item?.get(), + ) + // AND the first note should not have changed (actual list state) + val notes = currentScreen.children?.first()?.closestState("notes") + assertEquals( + mapOf("id" to noteId, "title" to noteTitle, "description" to noteDescription), + (notes?.get() as List).firstOrNull(), + ) + // AND the first page callback should not have been called + assertEquals(null, savedNote) + } +} diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/navigationCallbackMock.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/navigationCallbackMock.kt new file mode 100644 index 0000000..a0c6b6d --- /dev/null +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/navigationCallbackMock.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.integration.navigation + +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel + +private const val LIST_SCREEN = """{ + "_:component": "forEach", + "state": { + "notes": [ + { + "id": 1, + "title": "My first note", + "description": "Description of my first note" + }, + { + "id": 2, + "title": "My second note", + "description": "Description of my second note" + } + ] + }, + "properties": { + "items": "@{notes}", + "key": "id" + }, + "children": [ + { + "_:component": "layout:touchable", + "id": "edit", + "properties": { + "onPress": [ + { + "_:action": "push", + "properties": { + "url": "/edit", + "state": { + "note": "@{item}" + }, + "events": { + "onNoteSaved": [ + { + "_:action": "test:saveNote", + "properties": { + "note": "@{onNoteSaved}" + } + }, + { + "_:action": "setState", + "properties": { + "path": "item.title", + "value": "@{onNoteSaved.title}" + } + }, + { + "_:action": "setState", + "properties": { + "path": "item.description", + "value": "@{onNoteSaved.description}" + } + } + ] + } + } + } + ] + }, + "children": [ + { + "_:component": "layout:text", + "id": "text", + "properties": { + "text": "@{item.title}: @{item.description}" + } + } + ] + } + ] +}""" + +private const val EDIT_SCREEN = """{ + "_:component": "layout:column", + "id": "form", + "state": { + "title": "@{note.title}", + "description": "@{note.description}" + }, + "children": [ + { + "_:component": "test:textInput", + "id": "title", + "properties": { + "label": "Title", + "value": "@{title}", + "onChange": [{ + "_:action": "setState", + "properties": { + "path": "title", + "value": "@{onChange}" + } + }] + } + }, + { + "_:component": "test:textInput", + "id": "description", + "properties": { + "label": "Description", + "value": "@{description}", + "onChange": [{ + "_:action": "setState", + "properties": { + "path": "description", + "value": "@{onChange}" + } + }] + } + }, + { + "_:component": "test:button", + "id": "save", + "properties": { + "label": "Save", + "onPress": [ + { + "_:action": "triggerViewEvent", + "properties": { + "event": "onNoteSaved", + "value": "@{object('title', title, 'description', description, 'id', note.id)}" + } + }, + { "_:action": "pop" } + ] + } + }, + { + "_:component": "test:button", + "id": "cancel", + "properties": { + "label": "Cancel", + "onPress": [{ "_:action": "pop" }] + } + } + ] +}""" + +val callbackServerMock = MockEngine { request -> + return@MockEngine when(request.url.toString()) { + "$BASE_URL/list" -> respond( + content = ByteReadChannel(LIST_SCREEN), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + "$BASE_URL/edit" -> respond( + content = ByteReadChannel(EDIT_SCREEN), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + else -> respond( + content = ByteReadChannel(""), + status = HttpStatusCode.NotFound, + ) + } +} diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/serverMock.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/serverMock.kt index a979c2c..12ca606 100644 --- a/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/serverMock.kt +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/integration/navigation/serverMock.kt @@ -280,7 +280,7 @@ private const val STATEFUL_NAVIGATION_1 = """{ "_:action": "push", "properties": { "url": "/stateful-navigation-2", - "params": { + "state": { "address": "Rua dos bobos, 0" } } diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/array.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/array.kt new file mode 100644 index 0000000..5a535c0 --- /dev/null +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/array.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.unity.operations + +import br.com.zup.nimbus.core.ui.coreUILibrary +import kotlin.test.Test +import kotlin.test.assertEquals + +private val array = coreUILibrary.getOperation("array")!! + +class ArrayOperationTest { + @Test + fun `should create array`() { + val input = listOf(1, 2, true, null, false, "test", 28L) + assertEquals(input, array(input)) + } +} diff --git a/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/object.kt b/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/object.kt new file mode 100644 index 0000000..a4ac803 --- /dev/null +++ b/src/commonTest/kotlin/br/com/zup/nimbus/core/unity/operations/object.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2023 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.zup.nimbus.core.unity.operations + +import br.com.zup.nimbus.core.ui.coreUILibrary +import kotlin.test.Test +import kotlin.test.assertEquals + +private val objectOperation = coreUILibrary.getOperation("object")!! + +class ObjectOperationTest { + @Test + fun `should create object`() { + val input = listOf( + "key1", 1, + "key2", 2, + "key3", 3, + ) + assertEquals( + mapOf( + "key1" to 1, + "key2" to 2, + "key3" to 3, + ), + objectOperation(input) + ) + } + + @Test + fun `should create object even if input has non-string keys`() { + val input = listOf( + true, 1, + "key2", 2, + 5, "hello", + 58L, false, + null, "test", + ) + assertEquals( + mapOf( + "true" to 1, + "key2" to 2, + "5" to "hello", + "58" to false, + "null" to "test", + ), + objectOperation(input) + ) + } + + @Test + fun `should create object with odd number of arguments`() { + val input = listOf( + "key1", 1, + "key2", + ) + assertEquals( + mapOf( + "key1" to 1, + "key2" to null, + ), + objectOperation(input) + ) + } + + @Test + fun `should use most recent value on repeated keys`() { + val input = listOf( + "key1", 1, + "key1", 2, + "key2", 3, + "key2", 4, + ) + assertEquals( + mapOf( + "key1" to 2, + "key2" to 4, + ), + objectOperation(input) + ) + } +}