test(jetbrains): add markdown stress leak coverage

This commit is contained in:
kirillk
2026-06-05 12:16:30 -04:00
parent 3b762484f3
commit cd80323c0f
2 changed files with 196 additions and 0 deletions
+16
View File
@@ -268,6 +268,22 @@ Tests for retained Swing components should assert:
- `update(model)` changes existing labels/body text without duplicating components.
- Updates while collapsed do not eagerly create lazy bodies.
- No-op updates, empty deltas, repeated hover values, and toggling non-expandable cards do not repaint/revalidate the whole view.
- Streaming/rebuilding surfaces additionally require stress + leak tests (see below).
### Stress and Leak Tests for Streaming UI
Session/transcript UI that streams updates or rebuilds its component tree (markdown
views, code blocks, transcript parts, collapsible cards) must ship stress + leak tests in
addition to behavior tests. These tests must:
- Drive many updates (hundreds of streamed deltas or `set` cycles) through the public API.
- Assert that retained component instances stay identical across updates (`assertSame`).
- Assert the component count stays bounded — no growth per update.
- Assert disposable-backed resources return to baseline after churn + clear/dispose.
For code editors, compare `EditorFactory.getInstance().allEditors.size` against a
baseline captured before the loop.
See `MdViewHybridStressTest` for the reference pattern.
### Platform Components and Utilities
@@ -0,0 +1,180 @@
package ai.kilocode.client.ui.md
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBHtmlPane
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.UIUtil
import javax.swing.Box
import javax.swing.JPanel
/**
* Stress + leak coverage for the hybrid markdown renderer.
*
* These tests drive many updates through the public [MdView] API and inspect the real
* Swing component tree to prove that:
* - retained component instances survive heavy streaming,
* - the component tree stays bounded (no per-update growth),
* - editors created for code blocks are released (no leak) after churn + clear.
*/
@Suppress("UnstableApiUsage")
class MdViewHybridStressTest : BasePlatformTestCase() {
private lateinit var view: MdView
private var disposed = false
override fun setUp() {
super.setUp()
view = MdViewFactory.hybrid()
disposed = false
}
override fun tearDown() {
try {
if (this::view.isInitialized && !disposed) Disposer.dispose(view)
} finally {
super.tearDown()
}
}
fun `test streaming a large mixed document token by token stays consistent`() {
val doc = buildString {
append("# Heading\n\n")
append("Intro paragraph with **bold** text.\n\n")
append("- one\n- two\n- three\n\n")
append("```kotlin\nval x = 1\n```\n\n")
append("middle prose paragraph\n\n")
append("```java\nclass A {}\n```\n\n")
append("closing prose")
}
for (token in doc.chunked(3)) view.append(token)
assertEquals(doc, view.markdown())
assertEquals(3, htmls().size)
assertEquals(2, scrolls().size)
assertEquals(4, struts().size) // blocks - 1
assertEquals(9, panel().componentCount) // 5 blocks + 4 struts = 2*5 - 1
val html = view.html()
assertTrue(html.contains("<h1>"))
assertTrue(html.contains("<ul>"))
assertTrue(html.contains("class A"))
assertFalse(html.contains("<hr"))
}
fun `test trailing appends retain prefix html and code instances`() {
view.set("intro\n\n```kotlin\nval x = 1\n```\n\ntail")
val intro = htmls().first()
val tail = htmls().last()
val editor = editors().single()
editor.getEditor(true)
repeat(100) { i -> view.append(" more$i") }
assertSame(intro, htmls().first())
assertSame(tail, htmls().last())
assertSame(editor, editors().single())
assertEquals(2, htmls().size)
assertEquals(1, scrolls().size)
assertFalse(editor.getEditor(true)!!.isDisposed)
assertTrue(view.markdown().contains("more99"))
}
fun `test repeated same structure set reuses single editor and stays bounded`() {
repeat(150) { i ->
view.set("```kotlin\nval x = $i\n```")
editors().single().getEditor(true)
}
val editor = editors().single()
repeat(50) { i -> view.set("```kotlin\nval y = $i\n```") }
assertSame(editor, editors().single())
assertEquals(1, scrolls().size)
assertEquals(1, panel().componentCount)
assertEquals("val y = 49", editor.text)
}
fun `test structural churn releases every editor after clear`() {
val base = EditorFactory.getInstance().allEditors.size
repeat(60) { i ->
view.set("```kotlin\nval x = $i\n```")
editors().single().getEditor(true)
view.set("```java\nclass A$i {}\n```")
editors().single().getEditor(true)
view.set("plain prose $i")
}
view.clear()
drainEdt()
assertTrue(scrolls().isEmpty())
assertTrue(htmls().isEmpty())
assertEquals(0, panel().componentCount)
assertEquals(base, EditorFactory.getInstance().allEditors.size)
}
fun `test streaming code body reuses one editor and keeps html in sync`() {
view.append("```java\n")
val pane = scrolls().single()
val editor = editors().single()
val body = StringBuilder()
repeat(100) { i ->
val line = "void m$i() {}\n"
body.append(line)
view.append(line)
}
assertSame(pane, scrolls().single())
assertSame(editor, editors().single())
assertEquals(body.toString().trimEnd('\n'), editor.text)
assertTrue(view.html().contains("void m0()"))
assertTrue(view.html().contains("void m99()"))
view.append("```")
assertSame(pane, scrolls().single())
assertSame(editor, editors().single())
}
fun `test style changes during streaming do not rebuild components`() {
view.append("intro\n\n```kotlin\nval x = 1\n```\n\n")
val intro = htmls().first()
val editor = editors().single()
editor.getEditor(true)
val styled = SessionEditorStyle.create(family = "Courier New", size = 18)
val current = SessionEditorStyle.current()
repeat(50) { i ->
view.append("line $i ")
view.applyStyle(if (i % 2 == 0) styled else current)
if (i % 5 == 0) view.resetStyles()
}
assertSame(intro, htmls().first())
assertSame(editor, editors().single())
assertFalse(editor.getEditor(true)!!.isDisposed)
assertEquals(2, htmls().size)
assertEquals(1, scrolls().size)
assertTrue(view.markdown().contains("line 49"))
}
private fun panel(): JPanel = view.component as JPanel
private fun scrolls(): List<JBScrollPane> = panel().components.filterIsInstance<JBScrollPane>()
private fun htmls(): List<JBHtmlPane> = panel().components.filterIsInstance<JBHtmlPane>()
private fun struts(): List<Box.Filler> = panel().components.filterIsInstance<Box.Filler>()
private fun editors(): List<EditorTextField> = scrolls().mapNotNull { it.viewport.view as? EditorTextField }
private fun drainEdt() {
UIUtil.dispatchAllInvocationEvents()
}
}