IntelliJ `AnAction` 클래스를 구현하거나 수정하고 `plugin.xml`에 등록하도록 안내합니다. 표시 이름과 설명은 메시지 번들에 두고, 필요하면 아이콘을 설정하며, 이벤트 코루틴 범위에서 중단 함수를 실행해 동기식 처리로 IDE가 멈추는 일을 방지합니다.
- IDE 메뉴에 새 동작 추가
- 동작 등록이나 아이콘 변경
- 동작 이름과 설명 정리
IDE Starter와 UI Driver를 사용해 IntelliJ UI 테스트를 작성하고 문제를 조사하는 안내입니다. 테스트 구성, UI 요소 검색과 범위 지정, 키보드 입력, 대기 처리, 실패 시 UI 상태 확인 방법을 다룹니다.
Write IntelliJ UI tests with IDE Starter or UI Driver.
실행 점수 미검증
설치 방법 보기이 안내서는 IDE Starter와 UI Driver를 사용해 IntelliJ IDE 화면을 조작하는 테스트를 작성하는 방법을 설명합니다. JUnit 5 테스트 구성, 테스트 대상 IDE 지정, 화면 요소를 찾는 선택자, 부모 요소 안으로 검색 범위를 좁히는 방법을 다룹니다. 프로젝트 가져오기와 인덱싱이 끝날 때까지 기다리기, 파일을 직접 열기, 동작 후 대기하기, 키보드 입력, IDE 내부 상태 읽기도 확인할 수 있습니다. 테스트를 분류하는 어노테이션과 시작 시 설정하는 VM 옵션, 실제 브라우저를 조작하는 예시도 포함합니다. 테스트가 실패하면 UI 계층, 로그, 스크린샷을 확인해 요소를 찾지 못한 원인을 조사하는 절차를 참고할 수 있습니다.
설정 화면이나 도구 모음처럼 실제 IntelliJ UI를 거치는 동작을 확인하는 테스트를 추가할 때 유용합니다. 화면 요소를 찾는 방법과 대기 방식을 통일하거나 UI 테스트 실패의 단서를 찾을 때도 활용할 수 있습니다.
테스트에는 빌드된 IDE가 필요합니다. 문서에 나온 실행 예시는 tests.cmd를 사용합니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 28일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
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, 재사용 컴포넌트 API, 지연 목록 키, 애니메이션과 성능을 개별 UI 변경에 맞춰 살펴봅니다.