diff --git a/.github/workflows/kover.yml b/.github/workflows/kover.yml new file mode 100644 index 0000000000..6f2095b8ab --- /dev/null +++ b/.github/workflows/kover.yml @@ -0,0 +1,29 @@ +name: Measure coverage + +on: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Generate kover coverage report + run: ./gradlew koverXmlReport + - name: Add coverage report to PR + id: kover + uses: mi-kas/kover-report@v1 + with: + path: ${{ github.workspace }}/build/reports/kover/xml/report.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: Code Coverage + update-comment: true \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d17e5f8356..f5b8e2d8d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,9 @@ plugins { // See api/API_README.md for details id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.12.0" + + // Coverage + id("org.jetbrains.kotlinx.kover") version "0.6.1" } group = "com.github.kwebio" @@ -64,6 +67,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + testImplementation("org.awaitility:awaitility:4.2.0") } tasks.dokkaHtml { diff --git a/src/main/kotlin/kweb/Kweb.kt b/src/main/kotlin/kweb/Kweb.kt index 191d911f98..21a532c0a8 100755 --- a/src/main/kotlin/kweb/Kweb.kt +++ b/src/main/kotlin/kweb/Kweb.kt @@ -291,7 +291,7 @@ class Kweb private constructor( message.callback != null -> { val (resultId, result) = message.callback val resultHandler = remoteClientState.handlers[resultId] - ?: error("No data handler for $resultId for client ${remoteClientState.id}") + ?: error("No resultHandler for $resultId, for client ${remoteClientState.id}") resultHandler(result) } message.keepalive -> { diff --git a/src/test/kotlin/kweb/HistoryTest.kt b/src/test/kotlin/kweb/HistoryTest.kt index 60b3fe9c86..8e47f87793 100644 --- a/src/test/kotlin/kweb/HistoryTest.kt +++ b/src/test/kotlin/kweb/HistoryTest.kt @@ -15,16 +15,18 @@ import org.openqa.selenium.WebDriver import org.openqa.selenium.WebElement import org.openqa.selenium.chrome.ChromeDriver import org.openqa.selenium.support.ThreadGuard +import org.awaitility.Awaitility.* +import java.time.Duration @ExtendWith(SeleniumJupiter::class) -class HistoryTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { +class HistoryTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { - val driver : WebDriver + val driver: WebDriver init { //ThreadGuard.protect ensures that the ChromeDriver can only be called by the thread that created it //This should make this test thread safe. - driver = ThreadGuard.protect(unprotectedDriver) + driver = ThreadGuard.protect(unprotectedDriver) } companion object { @@ -47,27 +49,30 @@ class HistoryTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { fun testBackButton() { historyTestApp.reloadCount.value shouldBe 0 driver.get("http://localhost:7665/0") - driver.findElement(By.tagName("a")).let { aElement -> - historyTestApp.url.value shouldBe "/0" - aElement.click() - Thread.sleep(100) - historyTestApp.url.value shouldBe "/1" - } - driver.findElement(By.tagName("a")).let { aElement -> - aElement.click() - Thread.sleep(100) - historyTestApp.url.value shouldBe "/2" - historyTestApp.reloadCount.value shouldBe 1 + + historyTestApp.url.value shouldBe "/0" + driver.findElement(By.tagName("a")).click() + + await().untilAsserted { historyTestApp.url.value shouldBe "/1" } + + await().pollInSameThread().untilAsserted { + // For some reason was getting a StaleElementReferenceException intermittently, so put this + // inaide an await() + driver.findElement(By.tagName("a")).click() } + + await().untilAsserted { historyTestApp.url.value shouldBe "/2" } + await().untilAsserted { historyTestApp.reloadCount.value shouldBe 1 } + driver.navigate().back() - Thread.sleep(100) - historyTestApp.url.value shouldBe "/1" - historyTestApp.reloadCount.value shouldBe 1 + + await().untilAsserted { historyTestApp.url.value shouldBe "/1" } + await().untilAsserted { historyTestApp.reloadCount.value shouldBe 1 } driver.navigate().forward() - Thread.sleep(100) - historyTestApp.url.value shouldBe "/2" - historyTestApp.reloadCount.value shouldBe 1 + + await().untilAsserted { historyTestApp.url.value shouldBe "/2" } + await().untilAsserted { historyTestApp.reloadCount.value shouldBe 1 } } } @@ -88,9 +93,11 @@ class HistoryTestApp { route { path("/{num}") { p -> render(p["num"]!!.toInt()) { num -> - a().apply { + a { + element { + href = "/${num + 1}" + } text("Next ($num)") - href = "/${num + 1}" } } } diff --git a/src/test/kotlin/kweb/HrefTest.kt b/src/test/kotlin/kweb/HrefTest.kt index 92145e570c..3b83f5951a 100644 --- a/src/test/kotlin/kweb/HrefTest.kt +++ b/src/test/kotlin/kweb/HrefTest.kt @@ -5,6 +5,7 @@ import io.github.bonigarcia.seljup.SeleniumJupiter import io.kotest.matchers.shouldBe import kweb.* import kweb.state.KVar +import org.awaitility.Awaitility import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -49,10 +50,9 @@ class HrefTest(@Arguments("--headless") private var unprotectedDriver: ChromeDri hrefTestApp.appUrl.value shouldBe "/" hrefTestApp.renderCount.value shouldBe 1 aElement.click() - Thread.sleep(100) - hrefTestApp.appUrl.value shouldBe "/two" + Awaitility.await().untilAsserted { hrefTestApp.appUrl.value shouldBe "/two" } // Page shouldn't have been re-rendered for a relative link - hrefTestApp.renderCount.value shouldBe 1 + Awaitility.await().untilAsserted { hrefTestApp.renderCount.value shouldBe 1 } } diff --git a/src/test/kotlin/kweb/InputCheckedTest.kt b/src/test/kotlin/kweb/InputCheckedTest.kt index 857499d806..755bfbd829 100644 --- a/src/test/kotlin/kweb/InputCheckedTest.kt +++ b/src/test/kotlin/kweb/InputCheckedTest.kt @@ -5,6 +5,8 @@ import io.github.bonigarcia.seljup.SeleniumJupiter import io.kotest.matchers.shouldBe import kweb.* import kweb.state.KVar +import org.awaitility.Awaitility +import org.awaitility.Awaitility.await import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -44,17 +46,13 @@ class InputCheckedTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) val input = driver.findElement(By.tagName("input")) inputCheckedTestApp.checkKVar.value shouldBe false input.click() - Thread.sleep(100) - inputCheckedTestApp.checkKVar.value shouldBe true - Thread.sleep(100) + await().untilAsserted { inputCheckedTestApp.checkKVar.value shouldBe true } input.click() - Thread.sleep(100) - inputCheckedTestApp.checkKVar.value shouldBe false + await().untilAsserted { inputCheckedTestApp.checkKVar.value shouldBe false } - input.getAttribute("checked") shouldBe null + await().untilAsserted { input.getAttribute("checked") shouldBe null } inputCheckedTestApp.checkKVar.value = true - Thread.sleep(100) - input.getAttribute("checked") shouldBe "true" + await().untilAsserted { input.getAttribute("checked") shouldBe "true" } } } diff --git a/src/test/kotlin/kweb/SelectValueTest.kt b/src/test/kotlin/kweb/SelectValueTest.kt index 72fad4c12b..322ae29e2a 100644 --- a/src/test/kotlin/kweb/SelectValueTest.kt +++ b/src/test/kotlin/kweb/SelectValueTest.kt @@ -5,6 +5,7 @@ import io.github.bonigarcia.seljup.SeleniumJupiter import io.kotest.matchers.shouldBe import kweb.* import kweb.state.KVar +import org.awaitility.Awaitility import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -43,14 +44,11 @@ class SelectValueTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) fun mainTest() { driver.get("http://localhost:7668/") val select = Select(driver.findElement(By.tagName("select"))) - selectValueTestApp.selectValue.value shouldBe "" - Thread.sleep(100) + Awaitility.await().untilAsserted { selectValueTestApp.selectValue.value shouldBe "" } select.selectByValue("cat") - Thread.sleep(100) - selectValueTestApp.selectValue.value shouldBe "cat" + Awaitility.await().untilAsserted { selectValueTestApp.selectValue.value shouldBe "cat" } select.selectByValue("dog") - Thread.sleep(100) - selectValueTestApp.selectValue.value shouldBe "dog" + Awaitility.await().untilAsserted { selectValueTestApp.selectValue.value shouldBe "dog" } } } diff --git a/src/test/kotlin/kweb/state/render/RenderEachTest.kt b/src/test/kotlin/kweb/state/render/RenderEachTest.kt index 81f0e9ecb7..7ff56e328d 100644 --- a/src/test/kotlin/kweb/state/render/RenderEachTest.kt +++ b/src/test/kotlin/kweb/state/render/RenderEachTest.kt @@ -6,6 +6,8 @@ import io.kotest.matchers.shouldBe import kweb.* import kweb.state.ObservableList import kweb.state.renderEach +import org.awaitility.Awaitility +import org.awaitility.Awaitility.await import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.openqa.selenium.By @@ -49,13 +51,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElements(By.tagName("button")) button[0].click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Moose" - labels[1].text shouldBe "Dog" - labels[2].text shouldBe "Cat" - labels[3].text shouldBe "Bear" - labels[4].text shouldBe "Horse" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Moose" + labels[1].text shouldBe "Dog" + labels[2].text shouldBe "Cat" + labels[3].text shouldBe "Bear" + labels[4].text shouldBe "Horse" + } server.close() } @@ -81,13 +84,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElements(By.tagName("button")) button[0].click() - Thread.sleep(50) val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe ("Dog") - labels[1].text shouldBe ("Cat") - labels[2].text shouldBe ("Bear") - labels[3].text shouldBe ("Horse") - labels[4].text shouldBe ("Moose") + await().untilAsserted { + labels[0].text shouldBe ("Dog") + labels[1].text shouldBe ("Cat") + labels[2].text shouldBe ("Bear") + labels[3].text shouldBe ("Horse") + labels[4].text shouldBe ("Moose") + } server.close() } @@ -113,13 +117,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElements(By.tagName("button")) button[0].click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Dog" - labels[1].text shouldBe "Cat" - labels[2].text shouldBe "Moose" - labels[3].text shouldBe "Bear" - labels[4].text shouldBe "Horse" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Dog" + labels[1].text shouldBe "Cat" + labels[2].text shouldBe "Moose" + labels[3].text shouldBe "Bear" + labels[4].text shouldBe "Horse" + } server.close() } @@ -145,9 +150,10 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElements(By.tagName("button")) button[0].click() - Thread.sleep(50) val labels = driver.findElements(By.tagName("h1")) - labels[1].text shouldBe "Horse" + await().untilAsserted { + labels[1].text shouldBe "Horse" + } server.close() } @@ -183,25 +189,29 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElements(By.tagName("button")) button[0].click() - Thread.sleep(50) - var labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Aardvark" - labels[1].text shouldBe "Cow" - labels[2].text shouldBe "Dog" - labels[3].text shouldBe "Elephant" + + await().pollInSameThread().untilAsserted { + var labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Aardvark" + labels[1].text shouldBe "Cow" + labels[2].text shouldBe "Dog" + labels[3].text shouldBe "Elephant" + } button[1].click() - Thread.sleep(50) - labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Cow" - labels[1].text shouldBe "Dog" - labels[2].text shouldBe "Elephant" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Cow" + labels[1].text shouldBe "Dog" + labels[2].text shouldBe "Elephant" + } button[2].click() - Thread.sleep(50) - labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Cow" - labels[1].text shouldBe "Dog" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Cow" + labels[1].text shouldBe "Dog" + } server.close() } @@ -227,13 +237,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElement(By.tagName("button")) button.click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Dog" - labels[1].text shouldBe "Cat" - labels[2].text shouldBe "Horse" - labels[3].text shouldBe "Bear" - labels[4].text shouldBe "Moose" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Dog" + labels[1].text shouldBe "Cat" + labels[2].text shouldBe "Horse" + labels[3].text shouldBe "Bear" + labels[4].text shouldBe "Moose" + } server.close() } @@ -259,13 +270,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElement(By.tagName("button")) button.click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Cat" - labels[1].text shouldBe "Bear" - labels[2].text shouldBe "Moose" - labels[3].text shouldBe "Horse" - labels[4].text shouldBe "Dog" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Cat" + labels[1].text shouldBe "Bear" + labels[2].text shouldBe "Moose" + labels[3].text shouldBe "Horse" + labels[4].text shouldBe "Dog" + } server.close() } @@ -291,13 +303,14 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElement(By.tagName("button")) button.click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels[0].text shouldBe "Horse" - labels[1].text shouldBe "Dog" - labels[2].text shouldBe "Cat" - labels[3].text shouldBe "Bear" - labels[4].text shouldBe "Moose" + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels[0].text shouldBe "Horse" + labels[1].text shouldBe "Dog" + labels[2].text shouldBe "Cat" + labels[3].text shouldBe "Bear" + labels[4].text shouldBe "Moose" + } server.close() } @@ -323,9 +336,10 @@ class RenderEachTest(@Arguments("--headless") unprotectedDriver: ChromeDriver) { driver.get("http://localhost:1234") val button = driver.findElement(By.tagName("button")) button.click() - Thread.sleep(50) - val labels = driver.findElements(By.tagName("h1")) - labels.size shouldBe 0 + await().pollInSameThread().untilAsserted { + val labels = driver.findElements(By.tagName("h1")) + labels.size shouldBe 0 + } server.close() }