From 3096a840091a093106599d68ed23540ca07ca8da Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Mon, 29 Jun 2026 17:10:23 +0200 Subject: [PATCH] WEB-78041 document remote-driver UI-test gotchas in driver-ui-tests skill Captures lessons from building the Figma connection UI test that the skill did not cover: reading IDE-side state via @Remote (the plugin= classloader field for content-module classes and the declared-class method-dispatch pitfall), matching text-bearing toolbar/title actions that render as ActionButtonWithText rather than ActionButton, seeding a registry flag at startup via a -D system property, and driving a real browser with Playwright from the test JVM. Edited the community source and re-rendered the generated skill copies. (cherry picked from commit b3fd5d34005c7f53b15a72fa22d1de4318bed0c2) GitOrigin-RevId: b4407dc0dc580ada86e64fd9a99296432c0b42ac --- .agents/skills/driver-ui-tests/SKILL.md | 44 +++++++++++++++++++++++++ .claude/skills/driver-ui-tests/SKILL.md | 44 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/.agents/skills/driver-ui-tests/SKILL.md b/.agents/skills/driver-ui-tests/SKILL.md index 7a773340b430..fec96381b109 100644 --- a/.agents/skills/driver-ui-tests/SKILL.md +++ b/.agents/skills/driver-ui-tests/SKILL.md @@ -108,6 +108,18 @@ val detailPane = ui.x { byClass("PluginDetailsPageComponent") } detailPane.x { byClass("InstallButton") }.click() ``` +## Finding toolbar / title-bar actions + +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. Match by visible text across both variants: + +```kotlin +// Matches both icon-only and text-bearing action buttons +fun Finder.statusButton(text: String) = + x("//div[(@class='ActionButtonWithText' or @class='ActionButton') and @visible_text='$text']") +``` + +The visible text is itself a reliable assertion signal — you usually do not need to read the backing service/state. + ## Keyboard Interactions ```kotlin @@ -250,6 +262,38 @@ waitFor("text to appear", 10.seconds) { } ``` +## Reading IDE state via `@Remote` + +To 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 classes need the `plugin` field.** Without it the class resolves against the platform/core classloader → `DriverIllegalStateException: No such class '' in plugin null`. + - Class in a plugin **content module**: `@Remote("", plugin = "/")` (e.g. `com.intellij.figma/intellij.figma.core`). + - Class in the **main / embedded** plugin module: `@Remote("", plugin = "")`. +- **Method dispatch resolves against the DECLARED `@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.) +- Add the plugin module as a TEST dependency so the FQNs resolve for code-insight. + +```kotlin +@Remote("com.example.MyAppService", plugin = "com.example.myplugin/com.example.myplugin.core") +interface MyAppServiceRef { + fun getConfig(): MyConfigRef +} +// driver.service(MyAppServiceRef::class).getConfig()... +``` + +## Enabling a registry flag at startup + +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): + +```kotlin +context.applyVMOptionsPatch { + addSystemProperty("my.feature.enabled", "true") +} +``` + +## Driving a real browser (Playwright) + +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. + ## Running Tests from Terminal Driver tests require a fully built IDE. There are several ways to run them: diff --git a/.claude/skills/driver-ui-tests/SKILL.md b/.claude/skills/driver-ui-tests/SKILL.md index cf35c16c2288..a750457dad0d 100644 --- a/.claude/skills/driver-ui-tests/SKILL.md +++ b/.claude/skills/driver-ui-tests/SKILL.md @@ -109,6 +109,18 @@ val detailPane = ui.x { byClass("PluginDetailsPageComponent") } detailPane.x { byClass("InstallButton") }.click() ``` +## Finding toolbar / title-bar actions + +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. Match by visible text across both variants: + +```kotlin +// Matches both icon-only and text-bearing action buttons +fun Finder.statusButton(text: String) = + x("//div[(@class='ActionButtonWithText' or @class='ActionButton') and @visible_text='$text']") +``` + +The visible text is itself a reliable assertion signal — you usually do not need to read the backing service/state. + ## Keyboard Interactions ```kotlin @@ -251,6 +263,38 @@ waitFor("text to appear", 10.seconds) { } ``` +## Reading IDE state via `@Remote` + +To 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 classes need the `plugin` field.** Without it the class resolves against the platform/core classloader → `DriverIllegalStateException: No such class '' in plugin null`. + - Class in a plugin **content module**: `@Remote("", plugin = "/")` (e.g. `com.intellij.figma/intellij.figma.core`). + - Class in the **main / embedded** plugin module: `@Remote("", plugin = "")`. +- **Method dispatch resolves against the DECLARED `@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.) +- Add the plugin module as a TEST dependency so the FQNs resolve for code-insight. + +```kotlin +@Remote("com.example.MyAppService", plugin = "com.example.myplugin/com.example.myplugin.core") +interface MyAppServiceRef { + fun getConfig(): MyConfigRef +} +// driver.service(MyAppServiceRef::class).getConfig()... +``` + +## Enabling a registry flag at startup + +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): + +```kotlin +context.applyVMOptionsPatch { + addSystemProperty("my.feature.enabled", "true") +} +``` + +## Driving a real browser (Playwright) + +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. + ## Running Tests from Terminal Driver tests require a fully built IDE. There are several ways to run them: