Skip to content

Commit e7925c0

Browse files
committed
feat(editor): Show history opens the run containing the clicked test and selects it
The lens imported the globally newest run, so clicking a test could open an unrelated run that didn't contain it. Now it scans saved history newest-first for the first run whose XML contains the test's locationUrl (off the EDT) and imports that; once the tree is built it selects the matching node so the user lands on the test they clicked. Falls back to the newest run when none match.
1 parent 260e01c commit e7925c0

3 files changed

Lines changed: 58 additions & 7 deletions

File tree

src/main/kotlin/com/github/xepozz/testo/tests/console/TestoChannelHistory.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ internal object TestoChannelHistory {
8383
// Pass the root so install() renders the whole imported tree's aggregate immediately, independent of the
8484
// async JTree selection (which is often still null at this instant).
8585
TestoChannelsUi.install(console, store, levelFilter, project, console, root)
86+
// If "Show history" was clicked on a specific test, select its node so the user lands on that test.
87+
val targetUrl = (console.properties as? TestoImportedConsoleProperties)?.targetUrl
88+
if (targetUrl != null && root != null) {
89+
val match = findByLocationUrl(root, targetUrl)
90+
val form = console.resultsViewer as? SMTestRunnerResultsForm
91+
if (match != null && form != null) {
92+
ApplicationManager.getApplication().invokeLater { form.selectAndNotify(match) }
93+
}
94+
}
8695
return
8796
}
8897
lastCount = count
@@ -104,6 +113,20 @@ internal object TestoChannelHistory {
104113
}
105114
}
106115

116+
// Find the node for a clicked test. Prefer an exact locationUrl match; fall back to a node whose url starts with the
117+
// target (a data-provider method whose datasets carry a " with data set #N" suffix), so selecting it shows the
118+
// method's aggregate.
119+
private fun findByLocationUrl(root: SMTestProxy, url: String): SMTestProxy? {
120+
var prefixMatch: SMTestProxy? = null
121+
var result: SMTestProxy? = null
122+
forEachDescendant(root) { proxy ->
123+
val loc = proxy.locationUrl
124+
if (loc == url) result = result ?: proxy
125+
else if (prefixMatch == null && loc != null && loc.startsWith(url)) prefixMatch = proxy
126+
}
127+
return result ?: prefixMatch
128+
}
129+
107130
/** Encodes the test's full "all" stream (and the icon/color of every channel it used) for [SMTestProxy.setMetainfo]. */
108131
private fun encode(store: ChannelOutputStore, key: String): String? {
109132
val chunks = store.allFor(key)

src/main/kotlin/com/github/xepozz/testo/tests/console/TestoHistoryImport.kt

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ import javax.swing.Icon
3434
* import. We reconstruct the same import, but build the console on a subclass that re-delegates `createImportActions`,
3535
* so an imported-history tab carries the exact toolbar a live run does.
3636
*/
37-
internal fun openTestoHistory(project: Project, file: VirtualFile) {
37+
internal fun openTestoHistory(project: Project, file: VirtualFile, targetUrl: String? = null) {
3838
try {
3939
val executor = DefaultRunExecutor.getRunExecutorInstance()
40-
val profile = TestoImportRunProfile(file, project, executor)
40+
val profile = TestoImportRunProfile(file, project, executor, targetUrl)
4141
ExecutionEnvironmentBuilder.create(project, executor, profile)
4242
.executor(executor)
4343
.target(profile.target)
@@ -48,6 +48,29 @@ internal fun openTestoHistory(project: Project, file: VirtualFile) {
4848
}
4949
}
5050

51+
/**
52+
* "Show history" for one test: open the most recent saved run that actually contains [url] (so clicking a test doesn't
53+
* land on an unrelated latest run), and once imported, select that test's node. Falls back to the newest run overall.
54+
* Scans files off the EDT (the largest history XML is sizeable), then imports on the EDT.
55+
*/
56+
internal fun openTestoHistoryForTest(project: Project, url: String) {
57+
com.intellij.openapi.application.ApplicationManager.getApplication().executeOnPooledThread {
58+
val root = com.intellij.execution.TestStateStorage.getTestHistoryRoot(project)
59+
val files = com.intellij.execution.testframework.sm.TestHistoryConfiguration.getInstance(project).files
60+
.map { File(root, it) }
61+
.filter { it.exists() }
62+
.sortedByDescending { it.lastModified() }
63+
val target = files.firstOrNull { f -> runCatching { f.readText().contains(url) }.getOrDefault(false) }
64+
?: files.firstOrNull()
65+
?: return@executeOnPooledThread
66+
com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater {
67+
val vf = com.intellij.openapi.vfs.LocalFileSystem.getInstance().refreshAndFindFileByIoFile(target)
68+
?: return@invokeLater
69+
openTestoHistory(project, vf, url)
70+
}
71+
}
72+
}
73+
5174
/**
5275
* Mirrors `AbstractImportTestsAction.ImportRunProfile` (reused for parsing the saved `<config>` and resolving the
5376
* target), but its first [getState] returns our [TestoImportedRunnableState] so the console is built on our properties.
@@ -58,6 +81,7 @@ internal class TestoImportRunProfile(
5881
file: VirtualFile,
5982
project: Project,
6083
private val executor: Executor,
84+
private val targetUrl: String? = null,
6185
) : RunProfile {
6286
private val inner = AbstractImportTestsAction.ImportRunProfile(file, project, executor)
6387
private val ioFile = VfsUtilCore.virtualToIoFile(file)
@@ -73,7 +97,7 @@ internal class TestoImportRunProfile(
7397
val config = inner.initialConfiguration
7498
if (!imported && config is SMRunnerConsolePropertiesProvider) {
7599
imported = true
76-
return TestoImportedRunnableState(config as RunConfiguration, ioFile)
100+
return TestoImportedRunnableState(config as RunConfiguration, ioFile, targetUrl)
77101
}
78102
// Re-run from an imported tab (second invocation): run the original configuration's tests, not a re-import.
79103
return config?.getState(executor, environment) ?: inner.getState(executor, environment)
@@ -87,13 +111,14 @@ internal class TestoImportRunProfile(
87111
private class TestoImportedRunnableState(
88112
private val initialConfig: RunConfiguration,
89113
private val file: File,
114+
private val targetUrl: String?,
90115
) : RunProfileState {
91116
override fun execute(executor: Executor, runner: ProgramRunner<*>): ExecutionResult {
92117
val handler = NopProcessHandler()
93118
val delegate = (initialConfig as SMRunnerConsolePropertiesProvider)
94119
.createTestConsoleProperties(DefaultRunExecutor.getRunExecutorInstance()) as TestoConsoleProperties
95120
val props = TestoImportedConsoleProperties(
96-
delegate, file, handler, initialConfig.project, initialConfig, delegate.testFrameworkName, executor,
121+
delegate, file, handler, initialConfig.project, initialConfig, delegate.testFrameworkName, executor, targetUrl,
97122
)
98123
Disposer.register(props, delegate)
99124

@@ -120,6 +145,8 @@ internal class TestoImportedConsoleProperties(
120145
config: RunConfiguration,
121146
frameworkName: String,
122147
executor: Executor,
148+
// The locationUrl of the test the "Show history" lens was clicked on, so installForImport can select its node.
149+
val targetUrl: String? = null,
123150
) : ImportedTestConsoleProperties(delegate, file, handler, project, config, frameworkName, executor) {
124151
override fun createImportActions(): Array<AnAction> = delegate.createImportActions()
125152
}

src/main/kotlin/com/github/xepozz/testo/ui/TestoHistoryCodeVisionProvider.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ class TestoHistoryCodeVisionProvider : CodeVisionProviderBase() {
7171
private fun historyHint(url: String): String? = "Show history"
7272

7373
override fun handleClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
74-
openLatestHistory(element.project)
75-
// TODO(follow-up): after the imported tree is built, select the specific test node via
76-
// TestResultsViewer.selectAndNotify matching SMTestProxy.getLocationUrl() == the test url.
74+
val function = element as? Function ?: return openLatestHistory(element.project)
75+
val url = TestoTestRunLineMarkerProvider.getLocationHint(function)
76+
// Open the most recent run that actually contains this test (not just the globally latest) and select its node.
77+
com.github.xepozz.testo.tests.console.openTestoHistoryForTest(element.project, url)
7778
}
7879

7980
/**

0 commit comments

Comments
 (0)