diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiText.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiText.kt index 762ac582dd30..117bde70ef14 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiText.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/UiText.kt @@ -24,6 +24,10 @@ class UiText(private val component: UiComponent, private val textData: TextData) component.click(textData.point) } + fun strictClick() { + component.strictClick(textData.point) + } + fun doubleClick() { component.doubleClick(textData.point) } diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt index f7c759268753..ae319ef0eb3e 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/UiComponent.kt @@ -425,6 +425,16 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard { } } + fun strictClick(point: Point? = null, mouseButton: RemoteMouseButton = RemoteMouseButton.LEFT) { + LOG.info("strictClick at $this${point?.let { ": $it" } ?: ""}") + if (point != null) { + withComponent { robot.strictClick(it, point) } + } + else { + withComponent { robot.strictClick(it, null) } + } + } + fun doubleClick(point: Point? = null) { LOG.info("Double click at $this${point?.let { ": $it" } ?: ""}") if (point != null) { @@ -474,6 +484,14 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard { } } + fun mousePressAndRelease(from: Point, to: Point) { + with(robot) { + withComponent { moveMouse(it, from) } + pressMouse(RemoteMouseButton.LEFT) + releaseMouse(RemoteMouseButton.LEFT) + } + } + fun getBackgroundColor(): Color { return withComponent { Color(it.getBackground().getRGB()) } } diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/elements/NotebookTableOutputUi.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/elements/NotebookTableOutputUi.kt index 15feb33fabbf..3f6da979075f 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/elements/NotebookTableOutputUi.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/elements/NotebookTableOutputUi.kt @@ -95,7 +95,7 @@ class NotebookTableOutputUi(data: ComponentData) : UiComponent(data) { ) { button.waitFound(30.seconds) } - button.click() + button.strictClick() waitFor("expect the cell [0,0] doesn't contain '$textBefore' anymore") { tableView.getValueAt(0, 0) != textBefore } diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/notebooks/NotebookEditorComponent.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/notebooks/NotebookEditorComponent.kt index 20033c82c545..0ad9666197d7 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/notebooks/NotebookEditorComponent.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/components/notebooks/NotebookEditorComponent.kt @@ -119,19 +119,19 @@ class NotebookEditorUiComponent(private val data: ComponentData) : JEditorUiComp driver.ui.pasteText(content) } - fun runAllCells(): Unit = runAllCells.click() + fun runAllCells(): Unit = runAllCells.strictClick() - fun runCell(): Unit = runAndSelectNext.click() + fun runCell(): Unit = runAndSelectNext.strictClick() - fun clearAllOutputs(): Unit = clearOutputs.click() + fun clearAllOutputs(): Unit = clearOutputs.strictClick() - fun restartKernel(): Unit = restartKernel.click() + fun restartKernel(): Unit = restartKernel.strictClick() - fun interruptKernel(): Unit = interruptKernel.click() + fun interruptKernel(): Unit = interruptKernel.strictClick() fun deleteFirstCell() { - notebookCellEditors.first().click() - deleteCell.click() + notebookCellEditors.first().strictClick() + deleteCell.strictClick() } fun restartHighlighting() { @@ -172,7 +172,7 @@ class NotebookEditorUiComponent(private val data: ComponentData) : JEditorUiComp fun clickOnCell(cellSelector: CellSelector) { val cellEditors = notebookCellEditors val cell = cellSelector(cellEditors) - cell.click() + cell.strictClick() } fun typeInCell( @@ -239,14 +239,14 @@ fun Driver.createNewNotebook(name: String = "New Notebook", type: NotebookType) waitFor("wait for project tree to load", 30.seconds) { getAllTexts().isNotEmpty() } - getAllTexts().first().click() + getAllTexts().first().strictClick() } } invokeAction(type.newNotebookActionId, false) popup().run { - x("//div[@accessiblename='Name']", JTextFieldUI::class.java).click() + x("//div[@accessiblename='Name']", JTextFieldUI::class.java).strictClick() keyboard { waitFor("expect $name in the popup") { @@ -279,10 +279,10 @@ fun Driver.createNewNotebookWithMouse(name: String = "New Notebook", type: Noteb waitFor { newFileButton.present() } - newFileButton.click() + newFileButton.strictClick() popup().run { - waitOneText("${type.typeName} Notebook").click() + waitOneText("${type.typeName} Notebook").strictClick() keyboard { waitFor("expect $name in the popup") { diff --git a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/Robot.kt b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/Robot.kt index 9aab7bed9f70..209a97c7a12a 100644 --- a/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/Robot.kt +++ b/platform/remote-driver/test-sdk/src/com/intellij/driver/sdk/ui/remote/Robot.kt @@ -20,6 +20,24 @@ interface Robot { fun click(component: Component, point: Point, button: RemoteMouseButton, times: Int) fun pressAndReleaseKey(keyCode: Int, vararg modifiers: Int) fun pressModifiers(modifierMask: Int) + /** + * Performs a strict click in the context of SmoothRobot by repeating the full + * mouse cycle until a `mouseClicked` event is observed. + * + * How it differs from `clickWithRetry`: + * - `clickWithRetry` treats any of `mousePressed`/`mouseReleased`/`mouseClicked` as success + * and stops retrying once any of these events fires. + * - `strictClickWithRetry` is stricter: it only considers `mouseClicked` as success and + * will keep retrying the full press→release sequence until that event is received + * (or attempts are exhausted). + * + * Why this is needed: some components trigger their action only when the release happens + * within the same target. If we stop on `pressed` or `released` alone, the intended action may not + * happen. + * + * !!! Don't use strictClick if the component should disappear after mouse release. + */ + fun strictClick(component: Component, point: Point?) fun pressMouse(mouseButton: RemoteMouseButton) fun pressMouse(component: Component, point: Point) diff --git a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/robot/SmoothRobot.kt b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/robot/SmoothRobot.kt index eb63a175ad84..8f7a3aaa9768 100644 --- a/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/robot/SmoothRobot.kt +++ b/plugins/performanceTesting/remote-driver/src/com/jetbrains/performancePlugin/remotedriver/robot/SmoothRobot.kt @@ -280,6 +280,64 @@ internal class SmoothRobot @JvmOverloads constructor( } } + /** + * Performs a strict click in the context of SmoothRobot by repeating the full + * mouse cycle until a `mouseClicked` event is observed. + * + * How it differs from `clickWithRetry`: + * - `clickWithRetry` treats any of `mousePressed`/`mouseReleased`/`mouseClicked` as success + * and stops retrying once any of these events fires. + * - `strictClickWithRetry` is stricter: it only considers `mouseClicked` as success and + * will keep retrying the full press→release sequence until that event is received + * (or attempts are exhausted). + * + * Why this is needed: some components trigger their action only when the release happens + * within the same target. If we stop on `pressed` or `released` alone, the intended action may not + * happen. + * + * !!! Don't use strictClick if the component should disappear after mouse release. + */ + + fun strictClick(component: Component, point: Point? = null) { + strictClickWithRetry(component, point) + } + + private fun strictClickWithRetry(component: Component, where: Point?) { + if (useInputEvents()) { + postClickEvent(component, MouseButton.LEFT_BUTTON, 1) + return + } + //we don't want to register mouse listener to component that doesn't have mouse listeners + //this will break event propagation to a parent component + if (component.mouseListeners.isEmpty()) { + moveMouseAndClick(component, where, MouseButton.LEFT_BUTTON, 1) + return + } + + var attempt = 0 + while (attempt < 3) { + val clickLatch = CountDownLatch(1) + val mouseListener = object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent?) { + clickLatch.countDown() + logger.info("Mouse clicked on $component") + } + } + + component.addMouseListener(mouseListener) + moveMouseAndClick(component, where, MouseButton.LEFT_BUTTON, 1) + val clicked = clickLatch.await(3, TimeUnit.SECONDS) + component.removeMouseListener(mouseListener) + + if (clicked) { + break + } + + logger.warn("Repeating click. Click was unsuccessful on $component") + attempt++ + } + } + private fun fastPressAndReleaseKey(keyCode: Int, vararg modifiers: Int) { val unifiedModifiers = unify(*modifiers) val updatedModifiers = Modifiers.updateModifierWithKeyCode(keyCode, unifiedModifiers)