diff --git a/.github/workflows/quality-check.yaml b/.github/workflows/quality-check.yaml index 8f1686534..b89906e3d 100644 --- a/.github/workflows/quality-check.yaml +++ b/.github/workflows/quality-check.yaml @@ -213,7 +213,7 @@ jobs: path: | **/build/reports/**/*.html **/build/reports/**/*.log - **/build/reports/*/*.png + **/build/reports/**/*.png **/build/idea-sandbox/system/**/*.log **/build/idea-sandbox/system-test/**/*.log **/video/**/*.avi diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DataSourceComboBox.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DataSourceComboBox.kt index 9d3016b81..8b32e394f 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DataSourceComboBox.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DataSourceComboBox.kt @@ -1,6 +1,8 @@ package com.mongodb.jbplugin.editor.inputs import com.intellij.database.dataSource.LocalDataSource +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.asSequence @@ -8,6 +10,7 @@ import com.intellij.sql.indexOf import com.intellij.ui.AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED import com.intellij.ui.components.JBLabel import com.mongodb.jbplugin.accessadapter.datagrip.adapter.isConnected +import com.mongodb.jbplugin.editor.models.ToolbarState import com.mongodb.jbplugin.editor.models.getToolbarModel import com.mongodb.jbplugin.i18n.Icons import com.mongodb.jbplugin.i18n.Icons.scaledToText @@ -98,15 +101,10 @@ class DataSourceComboBox( var isFirstInit = true coroutineScope.launch { project.getToolbarModel().toolbarState.collect { state -> - withoutSelectionChangedListener { - selectedDataSourceConnecting = state.selectedDataSourceConnecting - selectedDataSourceFailedConnecting = state.selectedDataSourceConnectionFailed - if (isFirstInit || state.dataSources != dataSources) { - isFirstInit = false - populateComboBoxWithDataSources(state.dataSources) - } - selectDataSourceByUniqueId(state.selectedDataSource?.uniqueId) - } + ApplicationManager.getApplication().invokeLater({ + updateComboBoxState(state, isFirstInit) + isFirstInit = false + }, ModalityState.stateForComponent(comboBoxComponent)) } } } @@ -120,11 +118,28 @@ class DataSourceComboBox( } } - private fun populateComboBoxWithDataSources(dataSources: List) { - comboBoxModel.removeAllElements() - // First item is purposely a null to render "Detach data source label" - comboBoxModel.addElement(null) - comboBoxModel.addAll(dataSources) + private fun updateComboBoxState(state: ToolbarState, isFirstInit: Boolean) = withoutSelectionChangedListener { + selectedDataSourceConnecting = state.selectedDataSourceConnecting + selectedDataSourceFailedConnecting = state.selectedDataSourceConnectionFailed + if (isFirstInit || state.dataSources != dataSources) { + populateComboBoxWithDataSources(state.dataSources.toSet()) + } + selectDataSourceByUniqueId(state.selectedDataSource?.uniqueId) + } + + private fun populateComboBoxWithDataSources(newDataSources: Set) { + val oldDataSources = dataSources.toSet() + + val dataSourcesToAdd = newDataSources - oldDataSources + val dataSourcesToRemove = oldDataSources - newDataSources + + dataSourcesToAdd.forEach { comboBoxModel.addElement(it) } + dataSourcesToRemove.forEach { comboBoxModel.removeElement(it) } + + if (comboBoxModel.getIndexOf(null) != 0) { + comboBoxModel.removeElement(null) + comboBoxModel.insertElementAt(null, 0) + } } private fun selectDataSourceByUniqueId(uniqueId: String?) { diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DatabaseComboBox.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DatabaseComboBox.kt index 2423f18d0..11183d87f 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DatabaseComboBox.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/editor/inputs/DatabaseComboBox.kt @@ -1,10 +1,13 @@ package com.mongodb.jbplugin.editor.inputs +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.asSequence import com.intellij.ui.AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED import com.intellij.ui.components.JBLabel +import com.mongodb.jbplugin.editor.models.ToolbarState import com.mongodb.jbplugin.editor.models.getToolbarModel import com.mongodb.jbplugin.i18n.Icons import com.mongodb.jbplugin.i18n.Icons.scaledToText @@ -78,14 +81,10 @@ class DatabaseComboBox( var isFirstInit = true coroutineScope.launch { project.getToolbarModel().toolbarState.collect { state -> - withoutSelectionChangedListener { - loadingDatabases = state.databasesLoadingForSelectedDataSource - if (isFirstInit || state.databases != databases) { - isFirstInit = false - populateComboBoxWithDatabases(state.databases) - } - selectDatabaseAndNotify(state.selectedDatabase) - } + ApplicationManager.getApplication().invokeLater({ + updateComboBoxState(state, isFirstInit) + isFirstInit = false + }, ModalityState.stateForComponent(comboBoxComponent)) } } } @@ -99,11 +98,27 @@ class DatabaseComboBox( } } - private fun populateComboBoxWithDatabases(databases: List) { - comboBoxModel.removeAllElements() - // First item is purposely a null to render "Detach data source label" - comboBoxModel.addElement(null) - comboBoxModel.addAll(databases) + private fun updateComboBoxState(state: ToolbarState, isFirstInit: Boolean) = withoutSelectionChangedListener { + loadingDatabases = state.databasesLoadingForSelectedDataSource + if (isFirstInit || state.databases != databases) { + populateComboBoxWithDatabases(state.databases.toSet()) + } + selectDatabaseAndNotify(state.selectedDatabase) + } + + private fun populateComboBoxWithDatabases(newDatabases: Set) { + val oldDatabases = databases.toSet() + + val databasesToAdd = newDatabases - oldDatabases + val databasesToRemove = oldDatabases - newDatabases + + databasesToAdd.forEach { comboBoxModel.addElement(it) } + databasesToRemove.forEach { comboBoxModel.removeElement(it) } + + if (comboBoxModel.getIndexOf(null) != 0) { + comboBoxModel.removeElement(null) + comboBoxModel.insertElementAt(null, 0) + } } private fun selectDatabaseAndNotify(database: String?) { diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/codeActions/impl/MongoDbRunQueryActionUiTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/codeActions/impl/MongoDbRunQueryActionUiTest.kt index 278fb75ff..76c4b8f64 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/codeActions/impl/MongoDbRunQueryActionUiTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/codeActions/impl/MongoDbRunQueryActionUiTest.kt @@ -2,15 +2,15 @@ package com.mongodb.jbplugin.codeActions.impl import com.intellij.remoterobot.RemoteRobot import com.mongodb.jbplugin.fixtures.* -import com.mongodb.jbplugin.fixtures.components.findJavaEditorToolbar -import com.mongodb.jbplugin.fixtures.components.findJavaEditorToolbarPopup -import com.mongodb.jbplugin.fixtures.components.findRunQueryGutter import com.mongodb.jbplugin.fixtures.components.idea.ideaFrame +import com.mongodb.jbplugin.fixtures.components.openRunQueryPopup import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration @UiTest @RequiresMongoDbCluster @@ -20,6 +20,7 @@ class MongoDbRunQueryActionUiTest { remoteRobot: RemoteRobot, url: MongoDbServerUrl, ) { + remoteRobot.ideaFrame().cleanDataSources() remoteRobot.ideaFrame().addDataSourceWithUrl(javaClass.simpleName, url) } @@ -34,8 +35,7 @@ class MongoDbRunQueryActionUiTest { remoteRobot.ideaFrame().openFile( "/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java" ) - remoteRobot.findRunQueryGutter(atLine = 24)!!.click() - val popup = remoteRobot.findJavaEditorToolbarPopup() + val popup = remoteRobot.openRunQueryPopup(atLine = 24) popup.cancel() } @@ -45,16 +45,11 @@ class MongoDbRunQueryActionUiTest { remoteRobot.ideaFrame().openFile( "/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java" ) - remoteRobot.findJavaEditorToolbar().detachDataSource() - remoteRobot.findRunQueryGutter(atLine = 24)!!.click() - // because we are disconnected, we should now try to connect - val popup = remoteRobot.findJavaEditorToolbarPopup() - popup.dataSources.selectItem( - javaClass.simpleName - ) + val popup = remoteRobot.openRunQueryPopup(atLine = 24) + popup.selectDataSource(javaClass.simpleName) popup.ok("Run Query", timeout = 1.minutes) // check that we open a console - eventually { + eventually(30.seconds.toJavaDuration()) { val currentEditor = remoteRobot.ideaFrame().currentTab() assertTrue(currentEditor.editor.fileName.startsWith("console")) } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/editor/javaEditor/JavaDriverToolbarVisibilityUiTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/editor/javaEditor/JavaDriverToolbarVisibilityUiTest.kt index 94b5bc2a3..a88f654e8 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/editor/javaEditor/JavaDriverToolbarVisibilityUiTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/editor/javaEditor/JavaDriverToolbarVisibilityUiTest.kt @@ -6,8 +6,7 @@ import com.mongodb.jbplugin.fixtures.components.findJavaEditorToolbar import com.mongodb.jbplugin.fixtures.components.idea.ideaFrame import com.mongodb.jbplugin.fixtures.components.isJavaEditorToolbarHidden import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes @@ -21,6 +20,7 @@ class JavaDriverToolbarVisibilityUiTest { remoteRobot: RemoteRobot, url: MongoDbServerUrl, ) { + remoteRobot.ideaFrame().cleanDataSources() remoteRobot.ideaFrame().addDataSourceWithUrl(javaClass.simpleName, url) } @@ -30,7 +30,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `shows the toolbar in a java file with references to the driver`(remoteRobot: RemoteRobot) { remoteRobot.ideaFrame().openFile( "/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java" @@ -40,7 +40,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `shows the toolbar in all the java files with references to the driver`( remoteRobot: RemoteRobot ) { @@ -63,7 +63,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `does not show the toolbar in a java file without references to the driver`( remoteRobot: RemoteRobot ) { @@ -74,7 +74,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `does show existing data sources in the combo box`( remoteRobot: RemoteRobot, url: MongoDbServerUrl, @@ -88,7 +88,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `does not show the database select on a java driver file`( remoteRobot: RemoteRobot, url: MongoDbServerUrl, @@ -102,7 +102,7 @@ class JavaDriverToolbarVisibilityUiTest { } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `does show the database select on a spring criteria file`( remoteRobot: RemoteRobot, url: MongoDbServerUrl, @@ -114,34 +114,31 @@ class JavaDriverToolbarVisibilityUiTest { val toolbar = remoteRobot.findJavaEditorToolbar() assertTrue(toolbar.hasDatabasesComboBox) - eventually(1.minutes.toJavaDuration()) { - // when we select a cluster, it will connect asynchronously - toolbar.dataSources.selectItem(javaClass.simpleName) - } + toolbar.selectDataSource(javaClass.simpleName) + eventually(1.minutes.toJavaDuration()) { // it can take a few seconds, we will retry every few milliseconds // but wait at least for a minute if we can't select a database - toolbar.databases.selectItem("admin") + toolbar.selectDatabase("admin") } } @Test - @RequiresProject("basic-java-project-with-mongodb") + @RequiresProject("basic-java-project-with-mongodb", smartMode = true) fun `shows the toolbar when a reference to the driver is added`( remoteRobot: RemoteRobot, url: MongoDbServerUrl, ) { - assertTrue(remoteRobot.isJavaEditorToolbarHidden()) - remoteRobot.ideaFrame().openFile( "/src/main/java/alt/mongodb/javadriver/NoDriverReference.java" ) + assertTrue(remoteRobot.isJavaEditorToolbarHidden()) val editor = remoteRobot.ideaFrame().currentTab().editor val textBeforeChanges = editor.text editor.insertTextAtLine(1, 0, "import com.mongodb.client.MongoClient;") - - remoteRobot.findJavaEditorToolbar() - editor.text = textBeforeChanges.replace("\n", "\\\n") + assertTrue(remoteRobot.findJavaEditorToolbar().isShowing) + editor.replaceText("import com.mongodb.client.MongoClient;", "") + assertTrue(remoteRobot.isJavaEditorToolbarHidden()) } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/AssertionExtensions.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/AssertionExtensions.kt index f94c22547..c5a58f31d 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/AssertionExtensions.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/AssertionExtensions.kt @@ -5,6 +5,7 @@ package com.mongodb.jbplugin.fixtures import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.waitFor import java.time.Duration @@ -21,6 +22,38 @@ fun RemoteRobot.closeAllOpenModals() { } } +/** + * Waits until the block function finishes successfully up to 1 second (or the provided timeout). + * + * Example usages: + * + * ```kt + * eventually("Doing something") { + * verify(mock).myFunction() + * } + * // with custom timeout + * eventually(description = "Doing something", timeout = Duration.ofSeconds(5)) { + * verify(mock).myFunction() + * } + * ``` + * + * @param timeout + * @param fn + * @param recovery + */ +fun eventually( + description: String, + timeout: Duration = Duration.ofSeconds(1), + recovery: () -> Unit = {}, + fn: (Int) -> Unit +) { + eventually(timeout, recovery) { attempt -> + step("$description, attempt=$attempt") { + fn(attempt) + } + } +} + /** * Waits until the block function finishes successfully up to 1 second (or the provided timeout). * @@ -43,11 +76,12 @@ fun RemoteRobot.closeAllOpenModals() { fun eventually( timeout: Duration = Duration.ofSeconds(1), recovery: () -> Unit = {}, - fn: () -> Unit + fn: (Int) -> Unit ) { + var attempt = 1 waitFor(timeout, Duration.ofMillis(50)) { val result = runCatching { - fn() + fn(attempt++) true }.getOrDefault(false) @@ -80,16 +114,19 @@ fun eventually( */ fun eventually( timeout: Duration = Duration.ofSeconds(1), - fn: () -> T -): T? = waitFor( - timeout, - Duration.ofMillis( - 50 - ) -) { - val result = runCatching { - fn() - } + fn: (Int) -> T +): T? { + var attempt = 1 + return waitFor( + timeout, + Duration.ofMillis( + 50 + ) + ) { + val result = runCatching { + fn(attempt++) + } - result.isSuccess to result.getOrNull() + result.isSuccess to result.getOrNull() + } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/FixtureExtensions.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/FixtureExtensions.kt index 836592fd7..9d98bcc28 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/FixtureExtensions.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/FixtureExtensions.kt @@ -10,6 +10,7 @@ import com.intellij.remoterobot.fixtures.Fixture import com.intellij.remoterobot.fixtures.JButtonFixture import com.intellij.remoterobot.search.locators.Locator import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.waitFor import com.mongodb.jbplugin.fixtures.components.idea.maybeIdeaFrame import org.owasp.encoder.Encode @@ -81,7 +82,9 @@ inline fun RemoteRobot.findVisible( * * @param section */ -fun RemoteRobot.openSettingsAtSection(section: String) { +fun RemoteRobot.openSettingsAtSection( + section: String +) = step("Open settings modal and section $section") { this.runJs( """ importClass(com.intellij.openapi.application.ApplicationManager) @@ -103,7 +106,7 @@ fun RemoteRobot.openSettingsAtSection(section: String) { * * @param actionId */ -fun RemoteRobot.invokeAction(actionId: String) { +fun RemoteRobot.invokeAction(actionId: String) = step("Invoke action with id '$actionId'") { val encodedActionId = Encode.forJavaScript(actionId) runJs( @@ -135,7 +138,7 @@ fun RemoteRobot.invokeAction(actionId: String) { * * @param absolutePath */ -fun RemoteRobot.openProject(absolutePath: String) { +fun RemoteRobot.openProject(absolutePath: String) = step("Open Project at path $absolutePath") { val encodedPath = Encode.forJavaScript(absolutePath) runJs( @@ -170,7 +173,7 @@ fun RemoteRobot.openProject(absolutePath: String) { /** * Closes the project and waits until properly closed. */ -fun RemoteRobot.closeProject() { +fun RemoteRobot.closeProject() = step("Closing any open project") { invokeAction("CloseProject") maybeTerminateButton() } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/InfoAndProgressPanelFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/InfoAndProgressPanelFixture.kt new file mode 100644 index 000000000..443f5ed7b --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/InfoAndProgressPanelFixture.kt @@ -0,0 +1,46 @@ +package com.mongodb.jbplugin.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import java.time.Duration + +@DefaultXpath(by = "class", xpath = "//div[@class='InfoAndProgressPanelImpl']") +@FixtureName("Status bar") +class InfoAndProgressPanelFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ContainerFixture(remoteRobot, remoteComponent) { + private val inlineProgressPanel by lazy { + find(byXpath("//div[@class='InlineProgressPanel']")) + } + + private fun isInlineProgressPanelEmpty(): Boolean { + return inlineProgressPanel.callJs( + """ + const children = component.getComponents(); + children.length === 0 || children.every(child => !child.isVisible()); + """.trimIndent(), + runInEdt = true + ) + } + + fun waitForInProgressTasksToFinish(timeout: Duration = Duration.ofMinutes(2)) { + step("Ensure in-progress tasks are finished") { + waitFor( + duration = timeout, + description = "In-Progress tasks to finish", + errorMessage = "In-Progress tasks did not finish" + ) { + isInlineProgressPanelEmpty() + } + } + } +} + +fun RemoteRobot.infoAndProgressPanel(): InfoAndProgressPanelFixture = find() diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MdbStepLogger.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MdbStepLogger.kt new file mode 100644 index 000000000..ea7cecf79 --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MdbStepLogger.kt @@ -0,0 +1,57 @@ +package com.mongodb.jbplugin.fixtures + +import com.intellij.remoterobot.stepsProcessing.StepProcessor +import com.intellij.remoterobot.stepsProcessing.log +import com.intellij.remoterobot.utils.Color +import com.intellij.remoterobot.utils.color + +class MdbStepLogger(private val indentVal: String = "------ ") : StepProcessor { + private val ignorableStepTitlesForExceptionLogging = setOf( + "Wait until Gradle project is in sync", + "AI Assistant", + // combobox steps that won't need exception logging + "Select ", + "Search ", + "Selecting Database", + "Selecting DataSource", + ) + private var indent = ThreadLocal.withInitial { 0 } + + private fun indents() = buildString { + repeat(indent.get()) { append(indentVal) } + } + + override fun doBeforeStep(stepTitle: String) { + log.info(indents() + stepTitle) + indent.set(indent.get().plus(1)) + } + + override fun doOnSuccess(stepTitle: String) { + } + + override fun doOnFail(stepTitle: String, e: Throwable) { + if (ignorableStepTitlesForExceptionLogging.any { stepTitle.contains(it) }) { + log.warn( + "${indents()}Failed on step: $stepTitle (${getClassFileNameAndMethod()})".color( + Color.RED + ) + ) + } else { + log.warn( + "${indents()}Failed on step: $stepTitle (${getClassFileNameAndMethod()})".color( + Color.RED + ), + e + ) + } + } + + override fun doAfterStep(stepTitle: String) { + indent.set(indent.get().minus(1)) + } + + private fun getClassFileNameAndMethod(): String { + return Thread.currentThread().stackTrace[3]?.let { "${it.fileName}_${it.methodName}" } + ?: "---" + } +} diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/RightToolWindowHeaderFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/RightToolWindowHeaderFixture.kt new file mode 100644 index 000000000..3b114d740 --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/RightToolWindowHeaderFixture.kt @@ -0,0 +1,55 @@ +package com.mongodb.jbplugin.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.fixtures.JButtonFixture +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import java.time.Duration + +@DefaultXpath(by = "class", xpath = "//div[@class='ToolWindowHeader'][.//div[@class='BaseLabel']]") +@FixtureName("Right Tool Window Header") +class RightToolWindowHeaderFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ContainerFixture(remoteRobot, remoteComponent) { + private val headerActionsContainer by lazy { + find( + byXpath("//div[@classhierarchy='javax.swing.JPanel -> javax.swing.JComponent']") + ) + } + + val hideButton by lazy { + step("Retrieving hide button from right tool window header") { + headerActionsContainer.moveMouse() + headerActionsContainer.find( + byXpath("//div[@tooltiptext='Hide']") + ) + } + } +} + +fun RemoteRobot.rightToolWindowHeader(): RightToolWindowHeaderFixture = find() + +fun RemoteRobot.maybeRightToolWindowHeader(): RightToolWindowHeaderFixture? = runCatching { + rightToolWindowHeader() +}.getOrNull() + +fun RemoteRobot.closeRightToolWindow() { + step("Closing right tool window") { + waitFor( + duration = Duration.ofMinutes(1), + description = "Right tool window to close", + errorMessage = "Right tool window did not close", + ) { + if (maybeRightToolWindowHeader()?.isShowing == true) { + maybeRightToolWindowHeader()?.hideButton?.click() + } + maybeRightToolWindowHeader()?.isShowing != true + } + } +} diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/UiTestExtensions.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/UiTestExtensions.kt index 9711c8ea3..6d2af53e9 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/UiTestExtensions.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/UiTestExtensions.kt @@ -10,11 +10,11 @@ import com.intellij.database.util.common.containsElements import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.fixtures.ContainerFixture import com.intellij.remoterobot.search.locators.byXpath -import com.intellij.remoterobot.stepsProcessing.StepLogger import com.intellij.remoterobot.stepsProcessing.StepWorker import com.intellij.remoterobot.utils.DefaultHttpClient.client import com.intellij.remoterobot.utils.keyboard import com.mongodb.jbplugin.fixtures.components.idea.ideaFrame +import com.mongodb.jbplugin.fixtures.components.openGradleToolWindow import okhttp3.Request import org.junit.jupiter.api.Tag import org.junit.jupiter.api.extension.* @@ -71,8 +71,8 @@ private class UiTestExtension : ): Any = remoteRobot override fun beforeAll(context: ExtensionContext?) { - if (!StepWorker.processors.containsElements { it is StepLogger }) { - StepWorker.registerProcessor(StepLogger()) + if (!StepWorker.processors.containsElements { it is MdbStepLogger }) { + StepWorker.registerProcessor(MdbStepLogger()) } remoteRobot = RemoteRobot(remoteRobotUrl) @@ -115,9 +115,12 @@ private class UiTestExtension : } override fun beforeEach(context: ExtensionContext?) { + val testMethod = + context?.requiredTestMethod ?: throw IllegalStateException("test method is null") + val testMethodName = testMethod.name val requiresProject = context - ?.requiredTestMethod + .requiredTestMethod ?.annotations ?.find { annotation -> annotation.annotationClass == RequiresProject::class @@ -126,18 +129,26 @@ private class UiTestExtension : remoteRobot.keyboard { escape() } remoteRobot.closeProject() - requiresProject?.let { + requiresProject?.let { project -> // If we have the @RequireProject annotation, load that project on startup remoteRobot.openProject( - Path("src/test/resources/project-fixtures/${it.value}").toAbsolutePath().toString(), + Path( + "src/test/resources/project-fixtures/${project.value}" + ).toAbsolutePath().toString(), ) - if (it.smartMode) { + if (project.smartMode) { remoteRobot.ideaFrame().disablePowerSaveMode() + remoteRobot.openGradleToolWindow().also { + it.ensureGradleProjectsAreSynced() + } + // This ideally should not be needed anymore, but we still perform this + // to wait for smart mode to kick-in before doing anything else. remoteRobot.ideaFrame().waitUntilProjectIsInSync() } - remoteRobot.ideaFrame().hideIntellijAiAd() + // Close any right tool window + remoteRobot.closeRightToolWindow() } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/DatabaseToolWindowFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/DatabaseToolWindowFixture.kt new file mode 100644 index 000000000..0c6e56869 --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/DatabaseToolWindowFixture.kt @@ -0,0 +1,96 @@ +package com.mongodb.jbplugin.fixtures.components + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.JButtonFixture +import com.intellij.remoterobot.fixtures.JPopupMenuFixture +import com.intellij.remoterobot.fixtures.JTreeFixture +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.steps.CommonSteps +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import com.intellij.remoterobot.utils.waitForIgnoringError +import java.time.Duration + +@DefaultXpath( + by = "class", + xpath = "//div[@class='InternalDecoratorImpl' and @accessiblename='Database Tool Window']" +) +class DatabaseToolWindowFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ContainerFixture(remoteRobot, remoteComponent) { + private val databaseTree by lazy { + find(byXpath("//div[@class='DatabaseViewTreeComponent']")) + } + + fun removeDataSourceAtIndex(index: Int) { + val oldSize = databaseTree.collectRows().size + val dataSourceAtIndex = databaseTree.collectRows()[index] + step("Removing DataSource $dataSourceAtIndex at index $index") { + waitForIgnoringError( + duration = Duration.ofMinutes(1), + description = "Data source to be removed", + errorMessage = "Data source was not removed" + ) { + databaseTree.rightClickRow(index) + + CommonSteps(remoteRobot).wait(1) + val popupMenu = remoteRobot.find( + byXpath("//div[@class='MyMenu']") + ) + popupMenu.select("Remove Data Source… ") + + CommonSteps(remoteRobot).wait(1) + val confirmationDialog = remoteRobot.find( + byXpath("//div[@class='MyDialog']") + ) + confirmationDialog.find( + byXpath("//div[@text='OK']") + ).click() + + databaseTree.collectRows().size == oldSize - 1 + } + } + } + + fun removeDataSourceByName(name: String) { + step("Removing DataSource $name") { + val indexToRemove = databaseTree.collectRows().indexOfFirst { it.contains(name) } + removeDataSourceAtIndex(indexToRemove) + } + } + + fun removeAllDataSources() { + step("Removing all data sources") { + while (databaseTree.collectRows().isNotEmpty()) { + removeDataSourceAtIndex(0) + } + } + } +} + +fun RemoteRobot.databaseToolWindow(): DatabaseToolWindowFixture = find() + +fun RemoteRobot.maybeDatabaseToolWindow(): DatabaseToolWindowFixture? = runCatching { + databaseToolWindow() +}.getOrNull() + +fun RemoteRobot.openDatabaseToolWindow(): DatabaseToolWindowFixture { + return step("Open database tool window") { + waitFor( + duration = Duration.ofMinutes(1), + description = "Database tool window to open", + errorMessage = "Database tool window did not open" + ) { + maybeDatabaseToolWindow()?.isShowing ?: run { + rightToolbar().databaseButton.click() + maybeDatabaseToolWindow()?.isShowing == true + } + } + + return@step databaseToolWindow() + } +} diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/GradleToolWindowFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/GradleToolWindowFixture.kt new file mode 100644 index 000000000..9841eb5b1 --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/GradleToolWindowFixture.kt @@ -0,0 +1,95 @@ +package com.mongodb.jbplugin.fixtures.components + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import com.intellij.remoterobot.utils.waitForIgnoringError +import com.mongodb.jbplugin.fixtures.infoAndProgressPanel +import java.time.Duration + +@DefaultXpath( + by = "class", + xpath = "//div[@class='InternalDecoratorImpl' and @accessiblename='Gradle Tool Window']" +) +@FixtureName("Gradle Tool Window") +class GradleToolWindowFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent, +) : ContainerFixture(remoteRobot, remoteComponent) { + val actionToolbar by lazy { + find( + byXpath("//div[contains(@myvisibleactions, 'To') or contains(@myvisibleactions, 'of')]") + ) + } + + val reloadGradleButton by lazy { + step("Retrieving an enabled reload gradle button") { + val button = actionToolbar.find(byXpath("//div[@myicon='refresh.svg']")) + waitFor( + duration = Duration.ofMinutes(1), + description = "Reload gradle button to be enabled", + errorMessage = "Reload gradle button was still disabled" + ) { + button.isEnabled() + } + button + } + } + + val projectTree by lazy { + find(byXpath("//div[@class='ExternalProjectTree']")) + } + + fun ensureGradleProjectsAreSynced() { + step("Ensuring gradle projects are synced") { + runCatching { + waitForIgnoringError( + duration = Duration.ofMinutes(2), + description = "Gradle projects to show up", + errorMessage = "Gradle projects did not show up", + ) { + !projectTree.hasText("Nothing to show") + } + }.isFailure + + step("Manually reload gradle projects") { + reloadGradleButton.click() + waitFor( + duration = Duration.ofMinutes(2), + description = "Gradle projects to show up after manual reload", + errorMessage = "Gradle projects to show up after manual reload", + ) { + !projectTree.hasText("Nothing to show") + } + } + + remoteRobot.infoAndProgressPanel().waitForInProgressTasksToFinish() + } + } +} + +fun RemoteRobot.gradleToolWindow(): GradleToolWindowFixture = find() + +fun RemoteRobot.maybeGradleToolWindow(): GradleToolWindowFixture? = runCatching { + gradleToolWindow() +}.getOrNull() + +fun RemoteRobot.openGradleToolWindow(): GradleToolWindowFixture { + return step("Open gradle tool window") { + waitFor( + duration = Duration.ofMinutes(1), + description = "Gradle tool window to open", + errorMessage = "Gradle tool window did not open" + ) { + maybeGradleToolWindow()?.isShowing ?: run { + rightToolbar().gradleButton.click() + maybeGradleToolWindow()?.isShowing == true + } + } + + return@step gradleToolWindow() + } +} diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbActionGutterFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbActionGutterFixture.kt index 6ff6ef822..a6069d48f 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbActionGutterFixture.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbActionGutterFixture.kt @@ -6,8 +6,11 @@ package com.mongodb.jbplugin.fixtures.components import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.fixtures.GutterIcon +import com.intellij.remoterobot.stepsProcessing.step import com.mongodb.jbplugin.fixtures.components.idea.ideaFrame import com.mongodb.jbplugin.fixtures.eventually +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration /** * Function that returns the run query gutter at a specific line if provided. If no line provided, @@ -17,15 +20,29 @@ import com.mongodb.jbplugin.fixtures.eventually * @return the specific gutter than can be clicked */ fun RemoteRobot.findRunQueryGutter(atLine: Int? = null) = eventually { - ideaFrame().currentTab() - .gutter.run { - atLine?.let { -// for some reason, gutter icons lines are always 2 minus the line in the editor :shrug: -// hide this somehow in this implementation - getIcons().find { - it.lineNumber == atLine - 2 && - it.description.contains("path=/icons/ConsoleRun") - } - } ?: getIcons().find { it.description.contains("path=/icons/ConsoleRun") } + step("Finding run query gutter, atLine=$atLine") { + ideaFrame().currentTab() + .gutter.run { + atLine?.let { + // for some reason, gutter icons lines are always 2 minus the line in the editor :shrug: + // hide this somehow in this implementation + getIcons().find { + it.lineNumber == atLine - 2 && + it.description.contains("path=/icons/ConsoleRun") + } + } ?: getIcons().find { it.description.contains("path=/icons/ConsoleRun") } + }!! + } +} + +fun RemoteRobot.openRunQueryPopup(atLine: Int? = null): MdbJavaEditorToolbarPopupFixture { + // We always deselect the current data source because otherwise clicking on gutter icon will + // do the action itself instead of opening the popup + return step("Opening run query popup, atLine=$atLine") { + findJavaEditorToolbar().selectDetachDataSource() + return@step eventually(10.seconds.toJavaDuration()) { + findRunQueryGutter(atLine)!!.click() + return@eventually findJavaEditorToolbarPopup() }!! + } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarFixture.kt index 2a821081f..79bb67792 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarFixture.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarFixture.kt @@ -12,6 +12,7 @@ import com.mongodb.jbplugin.fixtures.eventually import com.mongodb.jbplugin.fixtures.findVisible import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration @@ -43,14 +44,33 @@ class MdbJavaEditorToolbarFixture( timeout = 50.milliseconds.toJavaDuration() ) }.isSuccess - fun detachDataSource() { - eventually { + + fun selectDataSource(title: String) { + eventually("Selecting DataSource $title in toolbar", 1.minutes.toJavaDuration()) { + dataSources.selectItemContains(title) + if (!dataSources.selectedText().contains(title)) { + throw Exception("Could not select data source - $title") + } + } + } + + fun selectDetachDataSource() { + eventually("Detaching DataSource from toolbar", 1.minutes.toJavaDuration()) { dataSources.selectItem("Detach data source") - if (!dataSources.selectedText().contains("Attach MongoDB")) { + if (dataSources.selectedText() != "") { throw Exception("Could not detach data source") } } } + + fun selectDatabase(title: String) { + eventually("Selecting Database $title in toolbar", 1.minutes.toJavaDuration()) { + databases.selectItemContains(title) + if (!databases.selectedText().contains(title)) { + throw Exception("Could not select database - $title") + } + } + } } /** diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarPopupFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarPopupFixture.kt index 5a45f7fa4..e96440199 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarPopupFixture.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MdbJavaEditorToolbarPopupFixture.kt @@ -5,9 +5,11 @@ import com.intellij.remoterobot.data.RemoteComponent import com.intellij.remoterobot.fixtures.* import com.intellij.remoterobot.search.locators.byXpath import com.intellij.remoterobot.utils.waitFor +import com.mongodb.jbplugin.fixtures.eventually import com.mongodb.jbplugin.fixtures.findVisible import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration @@ -50,7 +52,35 @@ class MdbJavaEditorToolbarPopupFixture( } button.click() } + fun cancel() = find(byXpath("//div[@text='Cancel']")).click() + + fun selectDataSource(title: String) { + eventually("Selecting DataSource $title in popup", 1.minutes.toJavaDuration()) { + dataSources.selectItemContains(title) + if (!dataSources.selectedText().contains(title)) { + throw Exception("Could not select data source - $title") + } + } + } + + fun selectDetachDataSource() { + eventually("Detaching DataSource from popup", 1.minutes.toJavaDuration()) { + dataSources.selectItem("Detach data source") + if (dataSources.selectedText() != "") { + throw Exception("Could not detach data source") + } + } + } + + fun selectDatabase(title: String) { + eventually("Selecting Database $title in popup", 1.minutes.toJavaDuration()) { + databases.selectItemContains(title) + if (!databases.selectedText().contains(title)) { + throw Exception("Could not select database - $title") + } + } + } } /** diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/RightToolbarFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/RightToolbarFixture.kt new file mode 100644 index 000000000..ea294300a --- /dev/null +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/RightToolbarFixture.kt @@ -0,0 +1,36 @@ +package com.mongodb.jbplugin.fixtures.components + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.fixtures.JButtonFixture +import com.intellij.remoterobot.search.locators.byXpath +import java.time.Duration + +@DefaultXpath( + by = "class", + xpath = "//div[@class='ToolWindowRightToolbar' or @accessiblename='Right Stripe']" +) +@FixtureName("RightToolbar") +class RightToolbarFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +) : ContainerFixture(remoteRobot, remoteComponent) { + val aiAssistantButton by lazy { + find(byXpath("//div[@tooltiptext='AI Assistant' or @text='AI Assistant']")) + } + + val databaseButton by lazy { + find(byXpath("//div[@tooltiptext='Database' or @text='Database']")) + } + + val gradleButton by lazy { + find(byXpath("//div[@tooltiptext='Gradle' or @text='Gradle']")) + } +} + +fun RemoteRobot.rightToolbar(): RightToolbarFixture = find( + Duration.ofSeconds(30) +) diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/idea/IdeaFrame.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/idea/IdeaFrame.kt index e92688e41..f5fa41080 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/idea/IdeaFrame.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/idea/IdeaFrame.kt @@ -9,14 +9,11 @@ package com.mongodb.jbplugin.fixtures.components.idea import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.data.RemoteComponent import com.intellij.remoterobot.fixtures.* -import com.intellij.remoterobot.search.locators.byXpath import com.intellij.remoterobot.steps.CommonSteps import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.waitFor import com.mongodb.jbplugin.fixtures.MongoDbServerUrl -import com.mongodb.jbplugin.fixtures.eventually import com.mongodb.jbplugin.fixtures.findVisible -import org.junit.jupiter.api.Assertions.assertTrue import org.owasp.encoder.Encode import java.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -36,41 +33,55 @@ class IdeaFrame( remoteComponent: RemoteComponent, ) : CommonContainerFixture(remoteRobot, remoteComponent) { fun openFile(path: String, closeOpenedFiles: Boolean = true) { - if (closeOpenedFiles) { - this.closeAllFiles() - } + step("Opening file at path $path with closeOpenedFiles=$closeOpenedFiles") { + if (closeOpenedFiles) { + this.closeAllFiles() + } - val escapedPath = Encode.forJavaScript(path) + val escapedPath = Encode.forJavaScript(path) - runJs( - """ - importPackage(com.intellij.openapi.fileEditor) - importPackage(com.intellij.openapi.vfs) - importPackage(com.intellij.openapi.wm.impl) - importClass(com.intellij.openapi.application.ApplicationManager) - importClass(com.intellij.openapi.fileEditor.FileEditorManager) - - const path = '$escapedPath' - const frameHelper = ProjectFrameHelper.getFrameHelper(component) - if (frameHelper) { - const project = frameHelper.getProject() - const projectPath = project.getBasePath() - const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path) - const openFileFunction = new Runnable({ - run: function() { - const fileEditorManager = FileEditorManager.getInstance(project); - const fileDescriptor = new OpenFileDescriptor( - project, - file - ); - fileEditorManager.openTextEditor(fileDescriptor, true) + val runResult = runCatching { + runJs( + """ + importPackage(com.intellij.openapi.fileEditor) + importPackage(com.intellij.openapi.vfs) + importPackage(com.intellij.openapi.wm.impl) + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.openapi.fileEditor.FileEditorManager) + importClass(com.intellij.openapi.application.ModalityState) + + const path = '$escapedPath' + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + const projectPath = project.getBasePath() + const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path) + const openFileFunction = new Runnable({ + run: function() { + const fileEditorManager = FileEditorManager.getInstance(project); + const fileDescriptor = new OpenFileDescriptor( + project, + file + ); + fileEditorManager.openTextEditor(fileDescriptor, true) + } + }) + ApplicationManager.getApplication().invokeAndWait(openFileFunction, ModalityState.current()) } - }) - ApplicationManager.getApplication().invokeAndWait(openFileFunction) + """, + true, + ) } - """, - true, - ) + + if (runResult.isFailure) { + if (runResult.exceptionOrNull()?.message?.contains("Write-unsafe context") != + true + ) { + throw runResult.exceptionOrNull()!! + } + } + } + CommonSteps(remoteRobot).wait(1) } fun currentTab(): TextEditorFixture = remoteRobot.findVisible( @@ -82,228 +93,229 @@ class IdeaFrame( name: String, url: MongoDbServerUrl, ) { - runJs( - """ - const LocalDataSourceManager = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.LocalDataSourceManager' - ) - - const DatabaseDriverManager = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.DatabaseDriverManager' - ) - - const LocalDataSource = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.LocalDataSource' - ) - - const DatabaseDriverValidator = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.validation.DatabaseDriverValidator' - ) - - const DatabaseConfigEditor = global.get('loadDataGripPluginClass')( - 'com.intellij.database.view.ui.DatabaseConfigEditor' - ) - - importClass(com.intellij.openapi.project.Project) - importPackage(com.intellij.openapi.progress) - importPackage(com.intellij.openapi.wm.impl) - importPackage(com.intellij.database.view.ui) + step("Adding DataSource with name=$name, url=$url") { + val runResult = runCatching { + runJs( + """ + const LocalDataSourceManager = global.get('loadDataGripPluginClass')( + 'com.intellij.database.dataSource.LocalDataSourceManager' + ) + + const DatabaseDriverManager = global.get('loadDataGripPluginClass')( + 'com.intellij.database.dataSource.DatabaseDriverManager' + ) + + const LocalDataSource = global.get('loadDataGripPluginClass')( + 'com.intellij.database.dataSource.LocalDataSource' + ) + + const DatabaseDriverValidator = global.get('loadDataGripPluginClass')( + 'com.intellij.database.dataSource.validation.DatabaseDriverValidator' + ) + + const DatabaseConfigEditor = global.get('loadDataGripPluginClass')( + 'com.intellij.database.view.ui.DatabaseConfigEditor' + ) + + importClass(com.intellij.openapi.project.Project) + importPackage(com.intellij.openapi.progress) + importPackage(com.intellij.openapi.wm.impl) + importPackage(com.intellij.database.view.ui) + + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + + const dataSourceManager = LocalDataSourceManager.getMethod("getInstance", Project).invoke(null, project) + const driverManager = DatabaseDriverManager.getMethod("getInstance").invoke(null) + const jdbcDriver = driverManager.getDriver("mongo") + + const dataSource = LocalDataSource.newInstance() + dataSource.setName("$name") + dataSource.setUrl("${url.value}") + dataSource.setConfiguredByUrl(true) + dataSource.setDatabaseDriver(jdbcDriver) + dataSourceManager.addDataSource(dataSource) + + global.put("dataSource", dataSource); + DatabaseDriverValidator.getMethod("createDownloaderTask", LocalDataSource, DatabaseConfigEditor) + .invoke(null, dataSource, null) + .run(new EmptyProgressIndicator()) + } + """.trimIndent(), + runInEdt = true, + ) + } - const frameHelper = ProjectFrameHelper.getFrameHelper(component) - if (frameHelper) { - const project = frameHelper.getProject() - - const dataSourceManager = LocalDataSourceManager.getMethod("getInstance", Project).invoke(null, project) - const driverManager = DatabaseDriverManager.getMethod("getInstance").invoke(null) - const jdbcDriver = driverManager.getDriver("mongo") - - const dataSource = LocalDataSource.newInstance() - dataSource.setName("$name") - dataSource.setUrl("${url.value}") - dataSource.setConfiguredByUrl(true) - dataSource.setDatabaseDriver(jdbcDriver) - dataSourceManager.addDataSource(dataSource) - - global.put("dataSource", dataSource); - DatabaseDriverValidator.getMethod("createDownloaderTask", LocalDataSource, DatabaseConfigEditor) - .invoke(null, dataSource, null) - .run(new EmptyProgressIndicator()) + if (runResult.isFailure) { + if (runResult.exceptionOrNull()?.message?.contains("Write-unsafe context") != + true + ) { + throw runResult.exceptionOrNull()!! + } } - """.trimIndent(), - runInEdt = true, - ) + } + CommonSteps(remoteRobot).wait(1) } - fun waitUntilConnectedToMongoDb(name: String, timeout: Duration = Duration.ofMinutes(1)) { - eventually(timeout) { - assertTrue( - callJs( + fun cleanDataSources() { + step("Removing all data sources") { + val runResult = runCatching { + runJs( """ - importClass(java.lang.System) + const LocalDataSourceManager = global.get('loadDataGripPluginClass')( + 'com.intellij.database.dataSource.LocalDataSourceManager' + ) + importClass(java.lang.System) + importClass(com.intellij.openapi.project.Project) + importClass(com.intellij.openapi.util.Key) + importPackage(com.intellij.openapi.progress) + importPackage(com.intellij.openapi.wm.impl) + importPackage(com.intellij.database.view.ui) + importClass(com.intellij.openapi.application.ApplicationManager) - const DatabaseConnectionManager = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.DatabaseConnectionManager' - ) - - const connectionManager = DatabaseConnectionManager.getMethod("getInstance").invoke(null) - const activeConnections = connectionManager.getActiveConnections() - var connected = false; - - for (connection of activeConnections) { - if(connection.getConnectionPoint().getDataSource().name.equals("$name")) { - try { - connected = !connection.getRemoteConnection().isClosed() && - connection.getRemoteConnection().isValid(10) - } catch (e) { - System.err.println(e.toString()) - } - - if (connected) { - break - } - } - } - - connected + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + const project = frameHelper.getProject() + const dataSourceManager = LocalDataSourceManager.getMethod("getInstance", Project).invoke(null, project) + const dataSources = dataSourceManager.getDataSources(); + for (let i = 0; i < dataSources.size(); i++) { + dataSourceManager.removeDataSource(dataSources.get(i)); + } """.trimIndent(), - runInEdt = true + runInEdt = true, ) - ) + } + if (runResult.isFailure) { + if (runResult.exceptionOrNull()?.message?.contains("Write-unsafe context") != + true + ) { + throw runResult.exceptionOrNull()!! + } + } } - - CommonSteps(remoteRobot).wait(1) } fun disablePowerSaveMode() { step("Disable Power Save Mode") { - runJs( - """ - importClass(com.intellij.ide.PowerSaveMode) - importClass(com.intellij.openapi.application.ApplicationManager) + val runResult = runCatching { + runJs( + """ + importClass(com.intellij.ide.PowerSaveMode) + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.openapi.application.ModalityState) + + const disableIt = new Runnable({ + run: function() { + PowerSaveMode.setEnabled(false) + } + }) - const disableIt = new Runnable({ - run: function() { - PowerSaveMode.setEnabled(false) - } - }) - - ApplicationManager.getApplication().invokeLater(disableIt) - """.trimIndent() - ) + ApplicationManager.getApplication().invokeLater(disableIt, ModalityState.current()) + """.trimIndent(), + runInEdt = true, + ) + } + + if (runResult.isFailure) { + if (runResult.exceptionOrNull()?.message?.contains("Write-unsafe context") != + true + ) { + throw runResult.exceptionOrNull()!! + } + } } + CommonSteps(remoteRobot).wait(1) } - fun waitUntilProjectIsInSync() { - eventually(timeout = Duration.ofMinutes(10)) { - step("Wait until Gradle project is in sync") { - assertTrue( - callJs( - """ + private fun waitUntilModulesAreLoaded(timeout: Duration = Duration.ofMinutes(1)) { + step("Wait until modules are loaded") { + waitFor( + duration = timeout, + description = "Modules to finish loading", + errorMessage = "Modules did not load" + ) { + callJs( + """ importPackage(com.intellij.openapi.wm.impl) importClass(com.intellij.openapi.module.ModuleManager) - const frameHelper = ProjectFrameHelper.getFrameHelper(component) const project = frameHelper.getProject() const modules = ModuleManager.getInstance(project).getModules() - + modules.length > 0 - """.trimIndent(), - runInEdt = true - ) + """.trimIndent(), + runInEdt = true ) } - - // exiting smart mode does not mean we are in smart mode! - // so try a few times and wish for luck, there is no better API it seems. - // Reasoning: you are in dumb mode, you load some project metadata, go to smart mode - // then realise that you have dependencies to download and index, so you go back to dumb - // mode until everything is done. - // happily enough, this won't take time if smart mode is already on, so it should - // be fast. - for (i in 0..10) { - CommonSteps(remoteRobot).waitForSmartMode(5) - } } } - fun hideIntellijAiAd() { - step("Hide IntelliJ AI Ad (uses a lot of space in a small window)") { - runCatching { - val aiMenu = remoteRobot.find( - byXpath("//div[@accessiblename='AI Assistant']") - ) - aiMenu.rightClick() - val hideAiMenu = remoteRobot.find(byXpath("//div[@class='MyList']")) - hideAiMenu.clickItem("Hide") + private fun waitForSmartMode(timeout: Int = 5) { + // exiting smart mode does not mean we are in smart mode! + // so try a few times and wish for luck, there is no better API it seems. + // Reasoning: you are in dumb mode, you load some project metadata, go to smart mode + // then realise that you have dependencies to download and index, so you go back to dumb + // mode until everything is done. + // happily enough, this won't take time if smart mode is already on, so it should + // be fast. + step("Optimistically waiting for smart mode") { + for (i in 0..10) { + CommonSteps(remoteRobot).waitForSmartMode(timeout) } } } - fun cleanDataSources() { - runJs( - """ - const LocalDataSourceManager = global.get('loadDataGripPluginClass')( - 'com.intellij.database.dataSource.LocalDataSourceManager' - ) - - importClass(java.lang.System) - importClass(com.intellij.openapi.project.Project) - importClass(com.intellij.openapi.util.Key) - importPackage(com.intellij.openapi.progress) - importPackage(com.intellij.openapi.wm.impl) - importPackage(com.intellij.database.view.ui) - importClass(com.intellij.openapi.application.ApplicationManager) - - const frameHelper = ProjectFrameHelper.getFrameHelper(component) - const project = frameHelper.getProject() - const dataSourceManager = LocalDataSourceManager.getMethod("getInstance", Project).invoke(null, project) - const dataSources = dataSourceManager.getDataSources(); - for (let i = 0; i < dataSources.size(); i++) { - dataSourceManager.removeDataSource(dataSources.get(i)); - } - """.trimIndent(), - runInEdt = true, - ) + fun waitUntilProjectIsInSync( + modulesTimeout: Duration = Duration.ofMinutes(10), + smartModeTimeout: Int = 5 + ) { + step("For project to be in sync") { + waitUntilModulesAreLoaded(modulesTimeout) + waitForSmartMode(smartModeTimeout) + } } fun closeAllFiles() { - runJs( - """ - importPackage(com.intellij.openapi.fileEditor) - importPackage(com.intellij.openapi.vfs) - importPackage(com.intellij.openapi.wm.impl) - importClass(com.intellij.openapi.application.ApplicationManager) - - const frameHelper = ProjectFrameHelper.getFrameHelper(component) - if (frameHelper) { - const project = frameHelper.getProject() - const closeEditorsFunction = new Runnable({ - run: function() { - const editorManager = FileEditorManager.getInstance(project) - const files = editorManager.openFiles - files.forEach((file) => { editorManager.closeFile(file) }) + step("Closing all files") { + val runResult = runCatching { + runJs( + """ + importPackage(com.intellij.openapi.fileEditor) + importPackage(com.intellij.openapi.vfs) + importPackage(com.intellij.openapi.wm.impl) + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.openapi.application.ModalityState) + + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + const closeEditorsFunction = new Runnable({ + run: function() { + const editorManager = FileEditorManager.getInstance(project) + const files = editorManager.openFiles + files.forEach((file) => { editorManager.closeFile(file) }) + } + }) + + ApplicationManager.getApplication().invokeLater( + closeEditorsFunction, + ModalityState.current() + ) } - }) - - ApplicationManager.getApplication().invokeLater(closeEditorsFunction) + """, + true, + ) } - """, - true, - ) - } - - fun ensureNotificationIsVisible(title: String) { - remoteRobot.findVisible(byXpath("//div[@visible_text='$title']")) - } - fun waitUntilNotificationIsGone(title: String, timeout: Duration = Duration.ofSeconds(2)) { - waitFor(timeout, interval = Duration.ofMillis(50)) { - runCatching { - !remoteRobot.find( - byXpath("//div[@visible_text='$title']") - ).isVisible() - }.getOrDefault(true) + if (runResult.isFailure) { + if (runResult.exceptionOrNull()?.message?.contains("Write-unsafe context") != + true + ) { + throw runResult.exceptionOrNull()!! + } + } } + CommonSteps(remoteRobot).wait(1) } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt index 5aedc64aa..9922ee396 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt @@ -6,8 +6,7 @@ import com.mongodb.jbplugin.fixtures.UiTest import com.mongodb.jbplugin.fixtures.components.openBrowserSettings import com.mongodb.jbplugin.fixtures.components.openSettings import com.mongodb.jbplugin.fixtures.components.useSetting -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @UiTest diff --git a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/build.gradle.kts b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/build.gradle.kts index 24571547b..3f086da0c 100644 --- a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/build.gradle.kts +++ b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/build.gradle.kts @@ -10,13 +10,10 @@ repositories { } dependencies { - implementation("org.mongodb:mongodb-driver-sync:5.1.1") - implementation("org.springframework.data:spring-data-mongodb:4.3.2") - - testImplementation(platform("org.junit:junit-bom:5.9.1")) - testImplementation("org.junit.jupiter:junit-jupiter") + implementation(libs.mongodb.driver) + implementation(libs.testing.spring.mongodb) } tasks.test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/settings.gradle.kts b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/settings.gradle.kts index ddee68b1b..3111545f0 100644 --- a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/settings.gradle.kts +++ b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/settings.gradle.kts @@ -1,2 +1,9 @@ rootProject.name = "basic-java-project-with-mongodb" +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../../../../../../gradle/libs.versions.toml")) // full path to the versions toml + } + } +}