Skip to content

Testing Guide

Patterns for testing editors, commands, state fields, and extensions.

Testing StateField Logic

StateField update logic can be tested without any UI:

@Test
fun testWordCount() {
    val wordCount = StateField.define(StateFieldSpec(
        create = { state -> countWords(state) },
        update = { _, tr -> countWords(tr.state) }
    ))

    val state = EditorState.create(EditorStateConfig(
        doc = DocSpec.StringDoc("hello world"),
        extensions = wordCount
    ))

    assertEquals(2, state.field(wordCount))
}

Testing Commands

Create an EditorSession and dispatch commands:

@Test
fun testDeleteLine() {
    val session = EditorSession(
        EditorState.create(EditorStateConfig(
            doc = DocSpec.StringDoc("line 1\nline 2\nline 3"),
            extensions = basicSetup
        ))
    )

    // Place cursor on line 2
    session.dispatch(TransactionSpec(
        selection = SelectionSpec.CursorSpec(7)
    ))

    // Run the command
    deleteLine(session)

    assertEquals("line 1\nline 3", session.state.sliceDoc())
}

Testing with EditorSession Convenience Methods

The convenience methods make test setup concise:

@Test
fun testInsertAt() {
    val session = EditorSession(
        EditorState.create(EditorStateConfig(
            doc = DocSpec.StringDoc("hello world")
        ))
    )

    session.insertAt(5, " beautiful")
    assertEquals("hello beautiful world", session.state.sliceDoc())
}

Testing Custom Completion Sources

@Test
fun testMyCompletionSource() {
    val source: CompletionSource = { ctx ->
        val match = ctx.matchBefore(Regex("[a-z]+"))
        if (match != null) {
            CompletionResult(
                from = match.from,
                options = listOf(
                    Completion(label = "hello"),
                    Completion(label = "help")
                )
            )
        } else null
    }

    val state = EditorState.create(EditorStateConfig(
        doc = DocSpec.StringDoc("hel")
    ))
    val ctx = CompletionContext(state, pos = 3, explicit = true)
    val result = source(ctx)

    assertNotNull(result)
    assertEquals(2, result.options.size)
    assertEquals(0, result.from)
}

Testing Linter Implementations

@Test
fun testMyLinter() {
    val myLinter: (EditorSession) -> List<Diagnostic> = { session ->
        val text = session.state.sliceDoc()
        val diagnostics = mutableListOf<Diagnostic>()
        val pattern = Regex("TODO")
        for (match in pattern.findAll(text)) {
            diagnostics.add(Diagnostic(
                from = match.range.first,
                to = match.range.last + 1,
                message = "Unresolved TODO",
                severity = Severity.WARNING
            ))
        }
        diagnostics
    }

    val session = EditorSession(
        EditorState.create(EditorStateConfig(
            doc = DocSpec.StringDoc("// TODO fix this")
        ))
    )
    val diagnostics = myLinter(session)
    assertEquals(1, diagnostics.size)
    assertEquals("Unresolved TODO", diagnostics[0].message)
}

Test Fixtures

Create reusable helpers for common test setups:

fun testSession(
    doc: String = "",
    extensions: Extension? = null
): EditorSession = EditorSession(
    EditorState.create(EditorStateConfig(
        doc = DocSpec.StringDoc(doc),
        extensions = extensions
    ))
)

// Usage:
val session = testSession("hello world", extensions = basicSetup)

Integration Testing with KodeMirror

For Compose UI tests, use ComposeTestRule:

@Test
fun testEditorRenders() = runComposeUiTest {
    setContent {
        val session = rememberEditorSession(doc = "test content")
        KodeMirror(session, modifier = Modifier.fillMaxSize())
    }

    // Verify the editor rendered
    onNodeWithText("test content").assertExists()
}

Tips

  • Prefer unit tests for StateField logic and commands — they're fast and don't need UI.
  • Use EditorSession directly (not rememberEditorSession) in tests — it doesn't require a Compose context.
  • Test edge cases: empty documents, single characters, very long lines, multi-byte characters, and multiple selections.