Testing Kotlin / JS: frameworks, coroutines and everything-everything-everything

Kotlin is a brilliant project. Initially conceived as just a JVM language, it later received compilation support for all mainstream platforms, including JavaScript.





Introductory. I have a pet project - a site and an API platform for the community for the game Elite: Dangerous. The backend is Kotlin / JVM (Ktor + Hibernate), the frontend is Kotlin / JS (KVision + Fomantic UI). I'll tell you about the pet project sometime later, and more on the front.





  • KVision is a frontend framework for Kotlin that combines ideas from various desktop frameworks (from Swing and JavaFX to WinForms and Flutter) and Kotlin syntax capabilities, such as DSL builders.





  • Fomantic-UI is a fork of Semantic-UI, a component HTML / JS web framework that can be compared to Bootstrap, only Fomantic is more interesting.





Not so long ago, I got the idea to link these two worlds and write a library for KVision, which would at least make it easier to write KVision pages with Fomantic elements. And, as befits an open source project, I planned to cover the library with tests. This article will be about this adventure.





The code

First of all, let's define the task. We have the following code for a simple Fomantic button in our hands:





package com.github.kam1sh.kvision.fomantic

import pl.treksoft.kvision.html.*
import pl.treksoft.kvision.panel.SimplePanel


open class FoButton(text: String) : Button(text = text, classes = setOf("ui", "button")) {
    var primary: Boolean = false
        set(value) {
            if (value) addCssClass("primary") else removeCssClass("primary")
            field = value
        }
}

fun SimplePanel.foButton(
    text: String,
    init: (FoButton.() -> Unit)? = null
): FoButton {
    val btn = FoButton(text)
    init?.invoke(btn)
    add(btn)
    return btn
}    

      
      



And there are a couple of tests:





package com.github.kam1sh.kvision.fomantic

import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.browser.document
import kotlinx.coroutines.*
import pl.treksoft.kvision.panel.ContainerType
import pl.treksoft.kvision.panel.Root

class FoButtonTest {
    lateinit var kvapp: Root

    @BeforeTest
    fun before() {
        kvapp = Root("kvapp", containerType = ContainerType.NONE, addRow = false)        
    }

    @Test
    fun genericButtonTest() {
        kvapp.foButton("Button")
        assertEqualsHtml("""...""")
    }

    @Test
    fun buttonPrimaryTest() {
        val btn = kvapp.foButton("Button") { primary = true }
        assertEqualsHtml("""...""")
        btn.primary = false
        assertEqualsHtml("""...""")
    }
}

fun assertEqualsHtml(expected: String, message: String? = null) {
    val actual = document.getElementById("kvapp")?.innerHTML
    assertEquals(expected, actual, message)
}
      
      



: "" KVision div id=kvapp, HTML-.





. div? HTML- - document.body?.insertAdjacentHTML(...)



, , - ?





<source lang="html">
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.css">
    <script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.js"></script>
</head>
<body>
    <main>
        <div id="kvapp">

        </div>
    </main>
</body>
</html>
</source>
      
      



You've lost karma

Kotlin/JS.

For browser projects, it downloads and installs the Karma test runner with other required dependencies; for Node.js projects, the Mocha test framework is used.

. Karma Mocha. , Karma js- karma.config.d



.





Karma , -:





// karma.config.d/page.js
config.set({
  customContextFile: "../../../../../src/test/resources/test.html"
})
      
      



test.html, , src/test/resources/test.html



. - , Karma build/js/packages/kvision-fomantic-test/node_modules



, .





, ? ./gradlew browserTest



, ... Disconnected (0 times), because no message in 30000 ms.





, HTML- , JS-. build/js/node_modules/karma/static/context.html



.





main-:





<!-- The scripts need to be in the body DOM element, as some test running frameworks need the body
     to have already been created so they can insert their magic into it. For example, if loaded
     before body, Angular Scenario test framework fails to find the body and crashes and burns in
     an epic manner. -->
<script src="context.js"></script>
<script type="text/javascript">
    // Configure our Karma and set up bindings
    %CLIENT_CONFIG%
    window.__karma__.setupContext(window);

    // All served files with the latest timestamps
    %MAPPINGS%
</script>
<!-- Dynamically replaced with <script> tags -->
%SCRIPTS%
<!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too.
This ensures all the tests will have been declared before karma tries to run them. -->
<script type="module">
    window.__karma__.loaded();
</script>
<script nomodule>
    window.__karma__.loaded();
</script>
      
      



, ... , .





- . ? , HTTP- Ktor . Python async



, pytest pytest-async, .





suspend .





> Task :compileTestKotlinJs FAILED





e: ...src/test/kotlin/com/github/kam1sh/kvision/fomantic/FoButtonTest.kt: (44, 5): Unsupported [suspend test functions]





- Gradle





. . .





, runBlocking {}



. ...





runBlocking Kotlin/JVM.





, , , , , by design. GlobalScope.promise



, runBlocking :





fun runBlocking(block: suspend (scope: CoroutineScope) -> Unit) = GlobalScope.promise { block(this) }
      
      



. , . Karma :





config.set({
  client: {
    mocha: {
      timeout: 9000
    }
  }
})
      
      



. workaround. UPD: @ilgonmic, , 0. !





Mocha, , , , :





  • , done-, .





  • , Promise.





, , . , kotlin-test-js .

, , . , Promise, Mocha .





, , ? -- ?





- Kotest. .





. , .





// build.gradle.kts
testImplementation("io.kotest:kotest-assertions-core-js:4.3.2")
testImplementation("io.kotest:kotest-framework-api-js:4.3.2")
testImplementation("io.kotest:kotest-framework-engine:4.3.2")
      
      



class FoButtonTest : FunSpec({
    var kvapp: Root? = null
    beforeEach {
        kvapp = Root("kvapp", containerType = ContainerType.NONE, addRow = false)
    }
    test("generic button") {
        kvapp!!.foButton("Button")
        assertEqualsHtml("""...""")
    }

    test("primary button") {
        val btn = kvapp!!.foButton("Button") { primary = true }
        assertEqualsHtml("""...""")
        btn.primary = false
        delay(200)
        assertEqualsHtml("""...""")
    }
})
      
      



kotest , , FunSpec -- .





, , delay() suspend.





:





That's all I can tell you about kotest for now. I will still develop the library, and when it is ready, I will announce its release in the Fomantic discord and the Kotlin / kvision slack. And in parallel I will learn kotest.





If this was not enough for you, then I apologize: I wanted to show the conclusion here ./gradlew --debug browserTest



, but the preparation of this material was already quite delayed due to the appearance of personal life, so if you are interested - contemplate the Gradle debug logs yourself.





So what? Well, nothing. Eat Kotlin / JS, drink kotest and take care of yourself.








All Articles