Testing

Render components with TestBed, query the DOM with KQuery, simulate events, and assert results.

The kraft-testing module provides two main tools: TestBed for rendering components into a real DOM, and KQuery for querying and interacting with the rendered elements.

KQuery is inspired by jQuery's "select and act" model. Tests run in a real browser via Karma/ChromeHeadless — so you're testing actual DOM behavior, not a mock. The flip side: tests are slower than pure unit tests.
// build.gradle.kts
jsTest {
    dependencies {
        implementation("io.peekandpoke.kraft:testing:$kraftVersion")
    }
}

TestBed

Render a component (or raw HTML) into a real browser DOM and run assertions against it:

@Test
fun counter_shows_initial_value() = TestBed.preact(
    view = { Counter(start = 5) },
) { root ->
    root.selectCss(".value").textContent() shouldBe "5"
}

The root parameter is a KQuery<Element> wrapping the rendered DOM tree. The test block is a suspend function — you can use delay() to wait for async state changes.

@Test
fun clicking_plus_increments() = TestBed.preact(
    view = { Counter(start = 0) },
) { root ->
    // Click the button
    root.selectCss("button.plus").click()
    delay(10)  // wait for re-render

    // Assert the result
    root.selectCss(".value").textContent() shouldBe "1"
}

KQuery — selecting elements

KQuery wraps a list of DOM elements. Every operation works on all matched elements — like jQuery. An empty query is safe: methods return empty lists or no-op on mutations.

// Select by CSS selector
val buttons = root.selectCss("button")
val inputs = root.selectCss<HTMLInputElement>("input[type=text]")

// Select by debug-id attribute (set via debugId() in ultra:html)
val header = root.selectDebugId("page-header")
val emailInput = root.selectDebugId<HTMLInputElement>("email-field")

// Select within results (chaining)
val submitBtn = root.selectCss("form").selectCss("button.submit")

KQuery — text & HTML

Method Returns Description
textContent(glue) String Combined text of all elements
containsText(text) Boolean Any element contains the text
innerHTML() List<String> Inner HTML of each element
outerHTML() List<String> Outer HTML of each element

KQuery — attributes & classes

Method Returns Description
attr(name) List<String?> Attribute value for each element
allHaveAttr(name) Boolean Every element has the attribute
anyHasAttr(name) Boolean Any element has the attribute
classes() List<Set<String>> CSS classes for each element
allHaveClass(cls) Boolean Every element has the class
anyHasClass(cls) Boolean Any element has the class

KQuery — form state

Method Returns Description
values() List<String?> Value of each input/textarea/select
setValue(val) KQuery Set value on all elements (no events fired)
typeText(text) KQuery Set value + fire input and change events
checkedStates() List<Boolean> Checked state of each input
allChecked() Boolean All inputs are checked
check() / uncheck() KQuery Set checked + fire change event
disabledStates() List<Boolean> Disabled state of each element
allDisabled() / allEnabled() Boolean Check enabled/disabled state
// Type into an input and verify the component reacts
root.selectCss("#search-input").typeText("hello")
delay(10)
root.selectCss(".results").textContent() shouldContain "hello"

// Check a checkbox
root.selectCss("#agree-checkbox").check()
delay(10)
root.selectCss("button.submit").allEnabled() shouldBe true

KQuery — traversal

Method Returns Description
parents() KQuery Parent elements (deduplicated)
children() KQuery All direct children
first() KQuery First element only
last() KQuery Last element only
nth(index) KQuery Element at index
filterElements { ... } KQuery Filter by predicate
// Get the third list item
root.selectCss("li").nth(2).textContent() shouldBe "Third"

// Get children of a container
root.selectCss("#list").children().size shouldBe 5

// Filter active items
root.selectCss(".item").filterElements { it.classList.contains("active") }

KQuery — event simulation

Every standard DOM event has a dispatch helper on KQuery. Events fire on all matched elements:

// Mouse events
root.selectCss("#btn").click()
root.selectCss("#area").dblClick()
root.selectCss("#handle").mouseDown()
root.selectCss("#target").mouseEnter()
root.selectCss("#target").mouseLeave()
root.selectCss("#menu").contextMenu()

// Keyboard events (with optional key/code)
root.selectCss("input").keyDown(key = "Enter")
root.selectCss("input").keyUp(key = "Escape")

// Focus
root.selectCss("input").focus()
root.selectCss("input").blur()

// Pointer events
root.selectCss("#canvas").pointerDown()
root.selectCss("#canvas").pointerMove()
root.selectCss("#canvas").pointerUp()

// Drag events
root.selectCss("#draggable").dragStart()
root.selectCss("#dropzone").dragEnter()
root.selectCss("#dropzone").drop()

// Touch events
root.selectCss("#slider").touchStart()
root.selectCss("#slider").touchMove()
root.selectCss("#slider").touchEnd()

// Clipboard
root.selectCss("#editor").copy()
root.selectCss("#editor").paste()

// Animation / transition
root.selectCss(".animated").animationEnd()
root.selectCss(".fading").transitionEnd()

// Scroll / load
root.selectCss(".scrollable").scroll()

// Low-level (any event type)
root.selectCss("#target").dispatch("customevent", bubbles = true)

Waiting for async content

Use awaitCss to poll for elements that appear after async operations:

// Wait up to 2 seconds for a loading spinner to disappear and content to appear
val items = root.awaitCss(".item-list", timeoutMs = 2000)
items.size shouldBeGreaterThan 0

Full example

class TodoAppSpec : StringSpec({

    "adding a todo shows it in the list" {
        TestBed.preact({ TodoApp() }) { root ->
            // Type a new todo
            root.selectCss("input.new-todo").typeText("Write tests")
            delay(10)

            // Submit the form
            root.selectCss("form").submit()
            delay(50)

            // Verify it appears in the list
            val items = root.selectCss(".todo-item")
            items.size shouldBe 1
            items.textContent() shouldContain "Write tests"
        }
    }

    "checking a todo marks it as done" {
        TestBed.preact({ TodoApp() }) { root ->
            // Add a todo first
            root.selectCss("input.new-todo").typeText("Test this")
            root.selectCss("form").submit()
            delay(50)

            // Check the checkbox
            root.selectCss(".todo-item input[type=checkbox]").check()
            delay(10)

            // Verify the done class is applied
            root.selectCss(".todo-item").allHaveClass("done") shouldBe true
        }
    }
})