实现或修改 IntelliJ `AnAction`,并在 `plugin.xml` 中注册。将显示名称和说明写入消息资源文件,按需设置图标,并通过事件协程作用域启动挂起任务,避免同步处理阻塞 IDE。
- 为 IDE 菜单添加操作
- 修改操作注册或图标
- 整理操作名称和说明
本指南说明如何使用 IDE Starter 和 UI Driver 编写、排查 IntelliJ 界面测试,涵盖测试结构、查找与限定界面元素、键盘输入、等待时机,以及失败时检查界面状态。
Write IntelliJ UI tests with IDE Starter or UI Driver.
实测评分 未实测
查看安装方法本指南介绍如何使用 IDE Starter 和 UI Driver 编写操作 IntelliJ IDE 界面的测试。内容包括 JUnit 5 测试结构、指定被测 IDE、查找界面元素的选择器,以及将搜索范围限定在父元素内。还介绍了等待项目导入和索引完成、直接打开文件、操作后等待、键盘输入,以及读取 IDE 内部状态的方法。指南也列出了用于分类测试的注解、启动时设置的 VM 选项,并提供操作真实浏览器的示例。测试失败时,可以按照相关步骤检查界面层级、日志和屏幕截图,调查未找到元素的原因。
如果您要添加通过真实 IntelliJ 界面验证行为的测试,例如设置页面或工具栏操作,可以参考本指南。需要统一元素定位和等待方式,或在界面测试失败后寻找线索时,也可以使用。
测试需要已构建的 IDE。文档中的运行示例使用 tests.cmd。
本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。
AI 根据 SKILL.md 的内容检查了需要注意的操作和使用前提。评分只依据实际运行的结果(实测对比)。
不计入评分。 由 AI(OpenAI Codex)于 2026年10月28日 评估。仅供参考,安装前请同时查看原文。
安装前可以查看交给 AI 代理的指令内容。
Guidelines for writing UI tests using IDE Starter and UI Driver frameworks.
// Driver core
import com.intellij.driver.client.Driver
import com.intellij.driver.sdk.waitForProjectOpen
import com.intellij.driver.sdk.advancedSettings
// Test utilities
import com.intellij.driver.tests.utils.waitForIndicators
import com.intellij.driver.tests.utils.Plugin
import com.intellij.driver.tests.utils.PluginInstaller
import com.intellij.driver.tests.utils.Plugins
// IDE Starter framework
import com.intellij.ide.starter.driver.runIdeTest
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.models.IDEStartResult
import com.intellij.ide.starter.models.VMOptions
import com.intellij.ide.starter.runner.IDERunContext
import com.intellij.ide.starter.runner.Starter
import com.intellij.ide.starter.utils.catchAll
// Extended test infrastructure
import com.intellij.ide.starter.extended.allure.AllureHelperExtended.step
import com.intellij.ide.starter.extended.allure.Subsystems
import com.intellij.ide.starter.extended.engine.newTestContainerExtended
import com.intellij.ide.starter.extended.engine.TestContainerExtended
import com.intellij.ide.starter.extended.license.StagingLicenseGenerator.licenseProductCode
import com.intellij.ide.starter.extended.loadMetadataFromServer
import com.intellij.ide.starter.extended.setupTestMetadataSchemeWithGroupsFromCode
// Test framework
import com.intellij.testFramework.TestApplicationManager
community/platform/remote-driver)com.intellij.ide.starter.models.TestCase see src/com/intellij/ide/starter/projectcommunity/tools/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeProductProvider.kt)See tests/intellij.ide.starter.extended/src/com/intellij/ide/starter/extended/data/cases
Page objects extend UiComponent with ComponentData constructor:
class MyPageObject(data: ComponentData) : UiComponent(data) {
val myButton = x { byAccessibleName("Button Name") }
val myPanel = x { byType("com.example.MyPanel") }
fun clickMyButton() {
myButton.click()
}
}
// Extension function on Finder to create the page object
fun Finder.myPageObject(): MyPageObject = x(
xQuery { byAccessibleName("Root Element Name") }, MyPageObject::class.java
)
// Use specific ui component to specify the context of the search
fun AnotherPageObject.myPageObject(): MyPageObject = x(
xQuery { byAccessibleName("Root Element Name") }, MyPageObject::class.java
)
| Selector | Usage |
|---|---|
byAccessibleName("name") | Find by accessible name attribute |
byJavaClass("com.example.MyPanel") | Find by the full class name |
byType("javax.swing.JTree") | Find by the full class name or a supertype |
byVisibleText("text") | Find by visible text content |
Use byJavaClass or byType. Do not use byClass, which is deprecated. A full class name has @Language("jvm-class-name"), so the IDE resolves it and updates it on rename.
Write the class name inside the query. Do not put it, or a full XPath, into a separate constant: the IDE cannot resolve or rename a class name in a plain string.
// BAD - a short class name in a separate constant
private const val POPUP_LIST_XPATH = "//div[@class='HeavyWeightWindow']//div[@class='MyList']"
// GOOD - a private helper with a resolvable class name
private fun Finder.optionPopup() = x { byType("com.intellij.ui.components.JBList") }
See directory tests/remote-driver-tests
See directory community/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui
When multiple elements match a selector, scope to a parent element:
// BAD - will fail if multiple InstallButtons exist
ui.x { byJavaClass("com.intellij.ide.plugins.newui.InstallButton") }.click()
// GOOD - scope to a parent element first
val detailPane = ui.x { byJavaClass("com.intellij.ide.plugins.newui.PluginDetailsPageComponent") }
detailPane.x { byJavaClass("com.intellij.ide.plugins.newui.InstallButton") }.click()
Toolbar and tool-window title actions that show their text (presentation.putClientProperty(ActionUtil.SHOW_TEXT_IN_TOOLBAR, true)) render as ActionButtonWithText, not ActionButton. The SDK actionButton(text) helper searches @class='ActionButton' only, so it silently never matches them. ActionButtonWithText extends ActionButton, so byType matches both variants:
// Matches both icon-only and text-bearing action buttons
fun Finder.statusButton(text: String) =
x { byType("com.intellij.openapi.actionSystem.impl.ActionButton") and byVisibleText(text) }
The visible text is itself a reliable assertion signal — you usually do not need to read the backing service/state.
element.keyboard { typeText("search text") }
ui.keyboard { key(KeyEvent.VK_ENTER) }
ui.keyboard { hotKey(KeyEvent.VK_META, KeyEvent.VK_COMMA) } // Cmd+,
Every UI test must have the following annotations at the class level:
| Annotation | Purpose | TestOps Custom Field |
|---|---|---|
@Subsystems.* | Categorizes the test by subsystem | Subsystem |
@Features.* | Specifies the feature being tested | Feature |
@Components.* | Identifies the component under test | Component |
For tests linked to TestOps test cases, also add:
@AllureId("test_case_id") - Links the test to the TestOps test case IDThe annotation values should match the corresponding TestOps custom fields (Subsystem, Feature, Component).
Available annotations: See tests/intellij.ide.starter.extended.allure/src/com/intellij/ide/starter/extended/allure/Annotations.kt
Example with TestOps test case:
@Subsystems.Java
@Features.Completion
@Components.Editor
class MyTestFromTestOps {
@Test
@AllureId("318541") // Required when test case exists in TestOps
fun `my test from testops`(testInfo: TestInfo) {
// ...
}
}
Example for new test (not yet in TestOps):
@Subsystems.UI
@Features.PluginManager
@Components.Miscellaneous
class MyNewTest {
@Test
fun `my new test`(testInfo: TestInfo) {
// ...
}
}
@Subsystems.Java
@Features.Completion
@Components.Editor
class MyTest {
val testCase = TestCase(IdeProductProvider.IU, myProject)
@Test
@AllureId("123456") // Required if test case exists in TestOps
fun `my test name`(testInfo: TestInfo) {
val context = Starter.newContext(testName = "TestName", testCase = testCase)
context.applyVMOptionsPatch {
addSystemProperty("ide.ui.non.modal.settings.window", "true")
}
context.runIdeTest(testName = testInfo.displayName) {
waitForIndicators(5.minutes) // Wait for indexing to complete
step("Step description") {
// Test actions here
}
}
}
}
Always wait for indicators at the start of your test:
waitForIndicators()
This ensures the project is fully imported and indexed before interacting with the IDE.
Use openFile instead of UI-based file navigation:
// GOOD - Direct and reliable
openFile(relativePath = "src/Main.java")
// AVOID - UI-based approach is slower and more fragile
invokeAction("GotoFile", now = false)
ui.keyboard { typeText("Main.java") }
ui.keyboard { key(KeyEvent.VK_ENTER) }
now ParameterThe now parameter controls whether the action completes before continuing:
// now = true: Waits for action to complete (use when keyboard input follows)
invokeAction("ToggleBookmarkWithMnemonic", now = true)
ui.keyboard { key(KeyEvent.VK_1) } // This input goes to the bookmark dialog
// now = false: Returns immediately (use when waiting for UI to appear)
invokeAction("ShowSettings", now = false)
ui.x { byJavaClass("com.intellij.openapi.options.newEditor.SettingsEditor") }.shouldBe { present() }
Rule: Use now = true when the next step is keyboard input to prevent input going to the wrong component.
Rule: Use now = false when you expect to the UI dialog to appear.
Use waitFor to wait for specific conditions:
waitFor("description of what we're waiting for", 30.seconds) {
ui.x { byJavaClass("com.example.MyComponent") }.present()
}
waitFor("text to appear", 10.seconds) {
ui.x { byType("javax.swing.JTree") }.hasText("expected text")
}
To show the actual value on failure, use the waitFor overload with getter and checker. It adds the last value to the error, so do not build errorMessage yourself:
// BAD - errorMessage repeats the getter
waitFor("the filter field holds '$query'", errorMessage = { "the filter field holds '${filterField.text}'" }) {
filterField.text == query
}
// GOOD - the error shows the last filterField.text
waitFor("the filter field holds '$query'", getter = { filterField.text }, checker = { it == query })
@RemoteTo read state from a service or model in the IDE under test, declare a @Remote interface and call it via driver.service(...) / driver.utility(...).
plugin field. Without it the class resolves against the platform/core classloader → DriverIllegalStateException: No such class '<fqn>' in plugin null.
@Remote("<fqn>", plugin = "<plugin.id>/<content.module>") (e.g. com.intellij.figma/intellij.figma.core).@Remote("<fqn>", plugin = "<plugin.id>").@Remote class, not the runtime object. A method declared on a sealed/abstract supertype ref is "not found" at runtime — declare it on the concrete subtype, or expose it via a top-level type. (The jvm-class-name injection also cannot resolve a nested Foo$Bar name → a cosmetic "Cannot resolve class" inspection error; prefer top-level types.)@Remote("com.example.MyAppService", plugin = "com.example.myplugin/com.example.myplugin.core")
interface MyAppServiceRef {
fun getConfig(): MyConfigRef
}
// driver.service(MyAppServiceRef::class).getConfig()...
Seed a registry key before the IDE starts with a -D VM option — RegistryValue falls back to System.getProperty. Required when a startup ProjectActivity or ToolWindowFactory.shouldBeAvailable reads the flag (setting it via the driver after start is too late):
context.applyVMOptionsPatch {
addSystemProperty("my.feature.enabled", "true")
}
Playwright runs in the test JVM, alongside the driver-driven IDE (both on localhost) — useful when the IDE's client is a web app/plugin. page.onConsoleMessage { ... } captures the page and its iframes (a strong diagnostic). Put custom screenshots/files under context.paths.testHome.resolve("log") so they are collected as test artifacts. See plugins/figma/integrationTests for a full example.
Driver tests require a fully built IDE. There are several ways to run them:
The tests.cmd script builds the IDE from sources and runs tests. Recommended for dev server mode.
Example:
./tests.cmd \
--module intellij.driver.tests \
--test com.intellij.driver.tests.idea.java.FindAndGoToTest
Key parameters:
--test - fully qualified test class name (or pattern)--module intellij.driver.tests - required for driver testsExample with specific test:
./tests.cmd \
--module intellij.driver.tests \
--test com.intellij.driver.tests.idea.ultimate.httpclient.BuiltInHttpClientBrotliCompressionUiTest
After test failure, check:
out/ide-tests/tests/{IDE-version}/{TestName}/{test-method}/log/ui-hierarchy/ui.htmlout/ide-tests/tests/{IDE-version}/{TestName}/{test-method}/log/idea.logout/ide-tests/tests/{IDE-version}/{TestName}/{test-method}/log/screenshots/out/ide-tests/tests/{IDE-version}/{TestName}/{test-method}/error/The ui-hierarchy/ui.html and full-screen.png written on failure are captured after useDriverAndCloseIde tears the IDE down — by then the session has ended and panels often revert to an empty/welcome state, so they can be misleading. Two better sources:
log/screenshots/001_heartbeat/ — captured mid-run, shows the real state during the wait.http://localhost:<port>/api/remote-driver/ (the harness sets -Dexpose.ui.hierarchy.url=true; the port is logged at startup as UI Hierarchy: http://localhost:<port>/api/remote-driver/). To inspect interactively, park the test at the point of interest — temporarily raise a waitFor timeout (e.g. to 20.minutes) — and curl/open that URL while the IDE stays alive. Each node exposes class (simple), javaclass (FQN, incl. Outer$Inner for inner classes), visible_text, and accessiblename; read these to build a reliable matcher instead of guessing from source.Thread.sleep() or delay() - Driver framework automatically waits for UI elementsstep("description") { } for better logsprivate – keep it in its test class. Put a reusable helper into a common UI componentwithRetries, keep it in the test module还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
实现或修改 IntelliJ `AnAction`,并在 `plugin.xml` 中注册。将显示名称和说明写入消息资源文件,按需设置图标,并通过事件协程作用域启动挂起任务,避免同步处理阻塞 IDE。
将 IntelliJ JPS 测试模块迁移到 Bazel,并排查仅在 Bazel 中出现的失败。根据症状检查依赖、插件、测试数据和 runfiles,重新生成并验证目标。
按 IntelliJ 项目规范编写或审查 Kotlin 和 Java 代码,涵盖语言选择、格式、密封类型、线程注解及控制流异常处理。
本指南帮助您按 IntelliJ 项目规则拆分更改并撰写提交信息:有工单时使用编号和变更说明,无工单时使用标签与子系统名,并在需要时说明原因和决定。
使用多个 Python 类型检查器比较代码片段或文件的类型推断和报错判断,并汇总各工具的结论、耗时与诊断信息,可保存为 Markdown 报告。
整理 Compose Desktop 界面中的状态和副作用,让可组合函数负责绘制,由展示器或协调器处理应用决策。了解状态归属、Effect、可复用组件接口、延迟列表键、动画和性能,适用于单项界面修改。