Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: UI test for feature availability in chat panel #5393

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions ui-tests-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@
id("toolkit-kotlin-conventions")
id("toolkit-intellij-plugin")

id("org.jetbrains.intellij.platform.base")
id("org.jetbrains.intellij.platform.module")
}

val ideProfile = IdeVersions.ideProfile(project)
val testPlugins by configurations.registering

// Add our source sets per IDE profile version (i.e. src-211)
sourceSets {
test {
java.srcDirs(findFolders(project, "tst", ideProfile))
resources.srcDirs(findFolders(project, "tst-resources", ideProfile))
java.setSrcDirs(findFolders(project, "tst-prep", ideProfile))
resources.setSrcDirs(findFolders(project, "tst-resources", ideProfile))
}
}

val uiTestSource = sourceSets.create("uiTest") {
java.setSrcDirs(findFolders(project, "tst", ideProfile))
}

idea {
module {
testSources.from(uiTestSource.allSource.srcDirs)
}
}

Expand All @@ -27,35 +37,57 @@
instrumentCode = false
}

tasks.initializeIntellijPlatformPlugin {
enabled = false
}
val uiTestImplementation by configurations.getting

Check notice on line 40 in ui-tests-starter/build.gradle.kts

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Function or property has platform type

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.

Check notice

Code scanning / QDJVMC

Function or property has platform type Note

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.

tasks.verifyPluginProjectConfiguration {
runtimeDirectory.set(null as File?)
enabled = false
configurations.getByName(uiTestSource.compileClasspathConfigurationName) {
extendsFrom(uiTestImplementation)
}

val testPlugins by configurations.registering
configurations.getByName(uiTestSource.runtimeClasspathConfigurationName) {
extendsFrom(uiTestImplementation)
}

dependencies {
// should really be set by the BOM, but too much work to figure out right now
testImplementation("org.kodein.di:kodein-di-jvm:7.20.2")
uiTestImplementation("org.kodein.di:kodein-di-jvm:7.20.2")
uiTestImplementation(platform(libs.junit5.bom))
uiTestImplementation(libs.junit5.jupiter)

intellijPlatform {
// shouldn't be needed? but IsolationException
val version = ideProfile.community.sdkVersion
intellijIdeaCommunity(version, !version.contains("SNAPSHOT"))
testFramework(TestFrameworkType.Starter)

localPlugin(project(":plugin-core"))
testImplementation(project(":plugin-core:core"))
testImplementation(project(":plugin-core:jetbrains-community"))
testImplementation(testFixtures(project(":plugin-core:jetbrains-community")))

testFramework(TestFrameworkType.Bundled)
testFramework(TestFrameworkType.JUnit5)

testFramework(TestFrameworkType.Starter, configurationName = uiTestImplementation.name)
}

testPlugins(project(":plugin-amazonq", "pluginZip"))
testPlugins(project(":plugin-core", "pluginZip"))
}

tasks.test {
dependsOn(testPlugins)
enabled = false
}

useJUnitPlatform()
val prepareAmazonQTest by intellijPlatformTesting.testIde.registering {
task {
useJUnitPlatform()
}
}

tasks.register<Test>("uiTest") {
testClassesDirs = uiTestSource.output.classesDirs
classpath = uiTestSource.runtimeClasspath

dependsOn(prepareAmazonQTest)
dependsOn(testPlugins)

systemProperty("ui.test.plugins", testPlugins.get().asPath)
systemProperty("org.gradle.project.ideProfileName", ideProfile.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.chatTests

import com.intellij.driver.sdk.waitForProjectOpen
import com.intellij.ide.starter.ci.CIServer
import com.intellij.ide.starter.config.ConfigurationStorage
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.driver.engine.runIdeWithDriver
import com.intellij.ide.starter.ide.IdeProductProvider
import com.intellij.ide.starter.junit5.hyphenateWithClass
import com.intellij.ide.starter.models.TestCase
import com.intellij.ide.starter.project.LocalProjectInfo
import com.intellij.ide.starter.runner.CurrentTestMethod
import com.intellij.ide.starter.runner.Starter
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.kodein.di.DI
import org.kodein.di.bindSingleton
import software.aws.toolkits.jetbrains.uitests.TestCIServer
import software.aws.toolkits.jetbrains.uitests.clearAwsXmlFile
import software.aws.toolkits.jetbrains.uitests.executePuppeteerScript
import software.aws.toolkits.jetbrains.uitests.setupTestEnvironment
import software.aws.toolkits.jetbrains.uitests.useExistingConnectionForTest
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths

class AmazonQChatTest {

init {
di = DI {
extend(di)
bindSingleton<CIServer>(overrides = true) { TestCIServer }
val defaults = ConfigurationStorage.instance().defaults.toMutableMap().apply {
put("LOG_ENVIRONMENT_VARIABLES", (!System.getenv("CI").toBoolean()).toString())
}

bindSingleton<ConfigurationStorage>(overrides = true) {
ConfigurationStorage(this, defaults)
}
}
}

@BeforeEach
fun setUp() {
// Setup test environment
setupTestEnvironment()
}

@Test
fun `Ensure feature availability on slash`() {
val testCase = TestCase(
IdeProductProvider.IC,
LocalProjectInfo(
Paths.get("tstData", "Hello")
)
).useRelease(System.getProperty("org.gradle.project.ideProfileName"))

// inject connection
useExistingConnectionForTest()

Starter.newContext(CurrentTestMethod.hyphenateWithClass(), testCase).apply {
System.getProperty("ui.test.plugins").split(File.pathSeparator).forEach { path ->
pluginConfigurator.installPluginFromPath(
Path.of(path)
)
}

copyExistingConfig(Paths.get("tstData", "configAmazonQTests"))
updateGeneralSettings()
}.runIdeWithDriver()
.useDriverAndCloseIde {
waitForProjectOpen()
// required wait time for the system to be fully ready
Thread.sleep(30000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt there a wait for index done or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a waitForIndicator() but that doesn't still doesn't add enough time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do this waitForIndicators(60.seconds) but I have had mixed results

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this in the next PR


val result = executePuppeteerScript(testFeatureAvailabilityOnSlash)
assertTrue(result.contains("/doc"))
assertTrue(result.contains("/dev"))
assertTrue(result.contains("/transform"))
assertTrue(result.contains("/help"))
assertTrue(result.contains("/clear"))
assertTrue(result.contains("/review"))
assertTrue(result.contains("/test"))

Thread.sleep(20000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the first sleep but why the one here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was mainly so that we could capture the exact output on the textbox cause otherwise it ends instantaneously. It doesnt need to be 20000 though, I'll try to make it shorter.

}
}

companion object {
@JvmStatic
@AfterAll
fun clearAwsXml() {
clearAwsXmlFile()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.chatTests

// language=JS
val testFeatureAvailabilityOnSlash = """
import puppeteer from "puppeteer";

async function testNavigation() {
const browser = await puppeteer.connect({
browserURL: "http://localhost:9222"
})
try {
const pages = await browser.pages()
for(const page of pages) {
const contents = await page.evaluate(el => el.innerHTML, await page.${'$'}(':root'));
const element = await page.$('.mynah-chat-prompt-input')
if(element) {
await page.type('.mynah-chat-prompt-input', '/')
const elements = await page.$$(".mynah-chat-command-selector-command");
const attr = await Promise.all(
elements.map(async element => {
return element.evaluate(el => el.getAttribute("command"));
})
);
console.log(JSON.stringify(attr, null, 2))
}
}
} finally {
await browser.close();
}
}
testNavigation().catch(console.error);

""".trimIndent()
51 changes: 51 additions & 0 deletions ui-tests-starter/tst-prep/PreAmazonQUiTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import com.intellij.openapi.Disposable
import com.intellij.testFramework.ApplicationExtension
import com.intellij.testFramework.junit5.TestDisposable
import org.junit.Rule
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.jetbrains.core.credentials.LegacyManagedBearerSsoConnection
import software.aws.toolkits.jetbrains.core.credentials.ManagedBearerSsoConnection
import software.aws.toolkits.jetbrains.core.credentials.pinning.ConnectionPinningManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
import software.aws.toolkits.jetbrains.utils.extensions.SsoLogin
import software.aws.toolkits.jetbrains.utils.extensions.SsoLoginExtension

@ExtendWith(ApplicationExtension::class, SsoLoginExtension::class)

Check failure on line 21 in ui-tests-starter/tst-prep/PreAmazonQUiTest.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unstable API Usage

'com.intellij.testFramework.ApplicationExtension' is scheduled for removal in a future version

Check failure

Code scanning / QDJVMC

Unstable API Usage Error

'com.intellij.testFramework.ApplicationExtension' is scheduled for removal in a future version
@SsoLogin("amazonq-test-account")
class PreAmazonQUiTest {

@TestDisposable
lateinit var disposable: Disposable

Check warning on line 26 in ui-tests-starter/tst-prep/PreAmazonQUiTest.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Property "disposable" is never used

Check warning

Code scanning / QDJVMC

Unused symbol Warning

Property "disposable" is never used

@Rule
@JvmField
val systemPropertyHelper = SystemPropertyHelper()

private lateinit var connection: ManagedBearerSsoConnection

@BeforeEach
fun setUp() {
System.setProperty("aws.dev.useDAG", "true")
}

@Test
fun `can set up Connection`() {
try {
val startUrl = System.getenv("TEST_START_URL")
val region = System.getenv("TEST_REGION")
connection = LegacyManagedBearerSsoConnection(startUrl, region, Q_SCOPES)
ConnectionPinningManager.getInstance().setPinnedConnection(QConnection.getInstance(), connection)
(connection.getConnectionSettings().tokenProvider.delegate as BearerTokenProvider).reauthenticate()
} catch (e: Exception) {
error("Could not connect to Idc.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests

import org.junit.jupiter.api.Assertions.assertEquals
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardOpenOption

private const val TEST_RESOURCES_PATH = "src/test/tstData"
fun executePuppeteerScript(scriptContent: String): String {
val scriptFile = File("$TEST_RESOURCES_PATH/temp-script.js")
scriptFile.parentFile.mkdirs()
scriptFile.writeText(scriptContent)

val process = ProcessBuilder()
.command("node", scriptFile.absolutePath)
.redirectErrorStream(true)
.start()

val output = process.inputStream.bufferedReader().use { it.readText() }
val exitCode = process.waitFor()

scriptFile.delete()

assertEquals(0, exitCode, "Script execution failed with output: $output")
return output
}

fun useExistingConnectionForTest() {
val testStartUrl = System.getenv("TEST_START_URL")
val testRegion = System.getenv("TEST_REGION")
val configContent =
"""
<application>
<component name="authManager">
<option name="ssoProfiles">
<list>
<ManagedSsoProfile>
<option name="scopes">
<list>
<option value="codewhisperer:conversations" />
<option value="codewhisperer:transformations" />
<option value="codewhisperer:taskassist" />
<option value="codewhisperer:completions" />
<option value="codewhisperer:analysis" />
</list>
</option>
<option name="ssoRegion" value="$testRegion" />
<option name="startUrl" value="$testStartUrl" />
</ManagedSsoProfile>
</list>
</option>
</component>
<component name="connectionPinningManager">
<option name="pinnedConnections">
<map>
<entry key="aws.codewhisperer" value="sso;$testRegion;$testStartUrl" />
<entry key="aws.q" value="sso;$testRegion;$testStartUrl" />
</map>
</option>
</component>
<component name="meetQPage">
<option name="shouldDisplayPage" value="false" />
</component>
</application>
""".trimIndent()
writeToAwsXml(configContent)
}

fun clearAwsXmlFile() {
val configContent =
"""
<application>

</application>
""".trimIndent()
writeToAwsXml(configContent)
}

fun setupTestEnvironment() {
// Ensure Puppeteer is installed
val npmInstall = ProcessBuilder()
.command("npm", "install", "puppeteer")
.inheritIO()
.start()
.waitFor()

assertEquals(0, npmInstall, "Failed to install Puppeteer")
}

fun writeToAwsXml(configContent: String) {
val path = Paths.get("tstData", "configAmazonQTests", "options", "aws.xml")

Files.createDirectories(path.parent)
Files.write(
path,
configContent.toByteArray(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
}
Loading
Loading