diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/Utils.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/Utils.kt new file mode 100644 index 00000000000..9d4d8efd7d3 --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/Utils.kt @@ -0,0 +1,25 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests + +import java.nio.file.Paths + +fun prepTestData(isCreate: Boolean) { + val process: Process + if (isCreate) { + val path = Paths.get("tstData", "qdoc", "createFlow", "README.md").toUri() + process = ProcessBuilder("rm", path.path).start() + } else { + val path = Paths.get("tstData", "qdoc", "updateFlow", "README.md").toUri() + process = ProcessBuilder("git", "restore", path.path).start() + } + + val exitCode = process.waitFor() + if (exitCode != 0) { + println("Warning: git stash command failed with exit code $exitCode") + process.errorStream.bufferedReader().use { reader -> + println("Error: ${reader.readText()}") + } + } +} diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/ScriptUtils.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/ScriptUtils.kt new file mode 100644 index 00000000000..860a75c4273 --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/ScriptUtils.kt @@ -0,0 +1,71 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests.scripts + +// language=TS +val findAndClickButtonScript = """ +const findAndClickButton = async ( + page, + buttonText, + clickButton = false, + timeout = 5000 +) => { + try { + // Wait for any matching buttons to be present + await page.waitForSelector('button.mynah-button', { + visible: true, + timeout, + }); + + // Find and verify the specific button + const buttonHandle = await page.evaluateHandle(text => { + const buttons = Array.from( + document.querySelectorAll('button.mynah-button') + ); + return buttons.find(button => { + const label = button.querySelector('.mynah-button-label'); + return label && label.textContent.trim() === text; + }); + }, buttonText); + + // Check if button was found + const button = buttonHandle.asElement(); + if (!button) { + console.log(buttonText); + throw new Error(`Button with text not found`); + } + + // Verify button is visible and enabled + const isVisible = await page.evaluate(el => { + const style = window.getComputedStyle(el); + return ( + style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' + ); + }, button); + + if (!isVisible) { + console.log(buttonText); + throw new Error(`Button with text is not visible`); + } + + if (clickButton) { + // Click the button + await button.click(); + + // Optional wait after click + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log(`Successfully clicked button with text`); + console.log(buttonText); + } else { + return button; + } + } catch (error) { + console.error(`Error interacting with button:`, buttonText, error); + throw error; + } +}; +""".trimIndent() diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithLatestChangesScripts.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithLatestChangesScripts.kt new file mode 100644 index 00000000000..270ccfce67e --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithLatestChangesScripts.kt @@ -0,0 +1,172 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests.scripts + +// language=TS +val updateReadmeLatestChangesConfirmOptionsScript = """ + const puppeteer = require('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) { + + console.log('Typing /doc in the chat window') + + await page.type('.mynah-chat-prompt-input', '/doc') + await page.keyboard.press('Enter') + + console.log('Attempting to find and click Update an existing README button') + await findAndClickButton(page, 'Update an existing README', true, 10000) + console.log('Attempting to find and click Update README to reflect code button') + await findAndClickButton(page, 'Update README to reflect code', true, 10000) + console.log('Attempting to find all available buttons') + const yesButton = await findAndClickButton(page, 'Yes', false, 10000) + const changeFolderButton = await findAndClickButton(page, 'Change folder', false, 10000) + const cancelButton = await findAndClickButton(page, 'Cancel', false, 10000) + + if (!yesButton || !changeFolderButton || !cancelButton) { + console.log('Error: Test Failed') + console.log('Unable to find buttons for Yes/ChangeFolder/Cancel') + } else { + console.log('Found all expected buttons') + console.log('Test Successful') + } + } + } + + } finally { + await browser.close(); + } + } + + testNavigation().catch((error) => { + console.log('Error: Test Failed'); + console.error(error); + }); +""".trimIndent() + +// language=TS +val updateReadmeLatestChangesScript = """ + + const puppeteer = require('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) { + + console.log('Typing /doc in the chat window') + + await page.type('.mynah-chat-prompt-input', '/doc') + await page.keyboard.press('Enter') + + console.log('Attempting to find and click Update an existing README button') + await findAndClickButton(page, 'Update an existing README', true, 10000) + console.log('Attempting to find and click Update README to reflect code button') + await findAndClickButton(page, 'Update README to reflect code', true, 10000) + console.log('Attempting to find and click Yes button to confirm option') + await findAndClickButton(page, 'Yes', true, 10000) + console.log('Waiting for updated README to be generated') + await new Promise(resolve => setTimeout(resolve, 90000)); + console.log('Attempting to find and click Accept button') + await findAndClickButton(page, 'Accept', true, 10000) + } + } + + } finally { + await browser.close(); + } + } + + testNavigation().catch((error) => { + console.log('Error: Test Failed'); + console.error(error); + }); + +""".trimIndent() + +// language=TS +val updateReadmeLatestChangesMakeChangesFlowScript = """ + + const puppeteer = require('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) { + + console.log('Typing /doc in the chat window'); + + await page.type('.mynah-chat-prompt-input', '/doc'); + await page.keyboard.press('Enter'); + + console.log('Attempting to find and click Update an existing README button'); + await findAndClickButton(page, 'Update an existing README', true, 10000); + console.log('Attempting to find and click Update README to reflect code button'); + await findAndClickButton(page, 'Update README to reflect code', true, 10000); + console.log('Attempting to find and click Yes button to confirm option'); + await findAndClickButton(page, 'Yes', true, 10000); + console.log('Waiting for updated README to be generated'); + await new Promise(resolve => setTimeout(resolve, 90000)); + console.log('Attempting to find and click Make changes button'); + await findAndClickButton(page, 'Make changes', true, 10000); + const makeChangeText = await page.waitForSelector('[placeholder="Describe documentation changes"]'); + if (!makeChangeText) { + console.log('Error: Test Failed'); + console.log('Unable to find placeholder description test in Make Changes flow'); + } else { + console.log('Found expected placeholder text for Make Changes flow'); + console.log('Test Successful'); + } + + } + } + + } finally { + await browser.close(); + } + } + + testNavigation().catch((error) => { + console.log('Error: Test Failed'); + console.error(error); + }); + +""".trimIndent() + +val updateReadmeLatestChangesConfirmOptionsTestScript = updateReadmeLatestChangesConfirmOptionsScript.plus(findAndClickButtonScript) +val updateReadmeLatestChangesTestScript = updateReadmeLatestChangesScript.plus(findAndClickButtonScript) +val updateReadmeLatestChangesMakeChangesFlowTestScript = updateReadmeLatestChangesMakeChangesFlowScript.plus(findAndClickButtonScript) diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithSpecificChangesScripts.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithSpecificChangesScripts.kt new file mode 100644 index 00000000000..c891a0df665 --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/scripts/UpdateReadmeWithSpecificChangesScripts.kt @@ -0,0 +1,128 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests.scripts + +// language=TS +val updateReadmeSpecificChangesMakeChangesFlowScript = """ + + const puppeteer = require('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) { + + console.log('Typing /doc in the chat window'); + + await page.type('.mynah-chat-prompt-input', '/doc'); + await page.keyboard.press('Enter'); + + console.log('Attempting to find and click Update an existing README button'); + await findAndClickButton(page, 'Update an existing README', true, 10000); + console.log('Attempting to find and click Make a specific change button'); + await findAndClickButton(page, 'Make a specific change', true, 10000); + console.log('Attempting to find and click Yes button to confirm option'); + await findAndClickButton(page, 'Yes', true, 10000); + + console.log('Typing specific change instructions in the chat window'); + await page.type('.mynah-chat-prompt-input', 'Add a section with Installation instructions for this repository. Title this new section \"### Installation\"'); + await page.keyboard.press('Enter'); + + console.log('Waiting for updated README to be generated'); + await new Promise(resolve => setTimeout(resolve, 90000)); + console.log('Attempting to find and click Make changes button'); + await findAndClickButton(page, 'Make changes', true, 10000); + const makeChangeText = await page.waitForSelector('[placeholder="Describe documentation changes"]'); + if (!makeChangeText) { + console.log('Error: Test Failed'); + console.log('Unable to find placeholder description text in Make Changes flow'); + } else { + console.log('Found expected placeholder text for Make Changes flow'); + console.log('Test Successful'); + } + + } + } + + } finally { + await browser.close(); + } + } + + testNavigation().catch((error) => { + console.log('Error: Test Failed'); + console.error(error); + }); + +""".trimIndent() + +// language=TS +val updateReadmeSpecificChangesScript = """ + + const puppeteer = require('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) { + + console.log('Typing /doc in the chat window'); + + await page.type('.mynah-chat-prompt-input', '/doc'); + await page.keyboard.press('Enter'); + + console.log('Attempting to find and click Update an existing README button'); + await findAndClickButton(page, 'Update an existing README', true, 10000); + console.log('Attempting to find and click Make a specific change button'); + await findAndClickButton(page, 'Make a specific change', true, 10000); + console.log('Attempting to find and click Yes button to confirm option'); + await findAndClickButton(page, 'Yes', true, 10000); + + console.log('Typing specific change instructions in the chat window'); + await page.type('.mynah-chat-prompt-input', 'Add a section with Installation instructions for this repository. Title this new section \"### Installation\"'); + await page.keyboard.press('Enter'); + + console.log('Waiting for updated README to be generated'); + await new Promise(resolve => setTimeout(resolve, 90000)); + + console.log('Attempting to find and click Accept button'); + await findAndClickButton(page, 'Accept', true, 10000); + } + } + + } finally { + await browser.close(); + } + } + + testNavigation().catch((error) => { + console.log('Error: Test Failed'); + console.error(error); + }); + +""".trimIndent() + +val updateReadmeSpecificChangesMakeChangesFlowTestScript = updateReadmeSpecificChangesMakeChangesFlowScript.plus(findAndClickButtonScript) +val updateReadmeSpecificChangesTestScript = updateReadmeSpecificChangesScript.plus(findAndClickButtonScript) diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeLatestChangesTest.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeLatestChangesTest.kt new file mode 100644 index 00000000000..3b76fe41c2d --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeLatestChangesTest.kt @@ -0,0 +1,190 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests.updateReadmeTests + +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.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +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.docTests.prepTestData +import software.aws.toolkits.jetbrains.uitests.docTests.scripts.updateReadmeLatestChangesConfirmOptionsTestScript +import software.aws.toolkits.jetbrains.uitests.docTests.scripts.updateReadmeLatestChangesMakeChangesFlowTestScript +import software.aws.toolkits.jetbrains.uitests.docTests.scripts.updateReadmeLatestChangesTestScript +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 UpdateReadmeLatestChangesTest { + init { + di = DI { + extend(di) + bindSingleton(overrides = true) { TestCIServer } + val defaults = ConfigurationStorage.instance().defaults.toMutableMap().apply { + put("LOG_ENVIRONMENT_VARIABLES", (!System.getenv("CI").toBoolean()).toString()) + } + + bindSingleton(overrides = true) { + ConfigurationStorage(this, defaults) + } + } + } + + @BeforeEach + fun setUpTest() { + // prep test data - restore readme contents + prepTestData(false) + } + + @Test + fun `Make Changes button leads to UPDATE with specific changes flow`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "qdoc", "updateFlow") + ) + ).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) + + val result = executePuppeteerScript(updateReadmeLatestChangesMakeChangesFlowTestScript) + + if (result.contains("Error: Test Failed")) { + println("result: $result") + } + + assertTrue(result.contains("Test Successful")) + assertFalse(result.contains("Error: Test Failed")) + } + } + + @Test + fun `Prompted to confirm selected folder`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "qdoc", "updateFlow") + ) + ).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) + + val result = executePuppeteerScript(updateReadmeLatestChangesConfirmOptionsTestScript) + + if (result.contains("Error: Test Failed")) { + println("result: $result") + } + + assertTrue(result.contains("Test Successful")) + assertFalse(result.contains("Error: Test Failed")) + } + } + + @Test + fun `UpdateReadme with latest changes returns an updated Readme`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "qdoc", "updateFlow") + ) + ).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) + + val result = executePuppeteerScript(updateReadmeLatestChangesTestScript) + + if (result.contains("Error: Test Failed")) { + println("result: $result") + } + + val readmePath = Paths.get("tstData", "qdoc", "updateFlow", "README.md") + val readme = File(readmePath.toUri()) + assertTrue(readme.exists()) + assertTrue(readme.readText().contains("sample/qdoc", ignoreCase = true)) + assertTrue(readme.readText().contains("healthcontroller.java", ignoreCase = true)) + } + } + + companion object { + @JvmStatic + @AfterAll + fun clearAwsXml() { + clearAwsXmlFile() + } + + @JvmStatic + @BeforeAll + fun setUpTestClass() { + // Setup test environment + setupTestEnvironment() + } + } +} diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeSpecificChangesTest.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeSpecificChangesTest.kt new file mode 100644 index 00000000000..da34bf27e12 --- /dev/null +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/docTests/updateReadmeTests/UpdateReadmeSpecificChangesTest.kt @@ -0,0 +1,150 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.uitests.docTests.updateReadmeTests + +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.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +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.docTests.prepTestData +import software.aws.toolkits.jetbrains.uitests.docTests.scripts.updateReadmeSpecificChangesMakeChangesFlowTestScript +import software.aws.toolkits.jetbrains.uitests.docTests.scripts.updateReadmeSpecificChangesTestScript +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 UpdateReadmeSpecificChangesTest { + init { + di = DI { + extend(di) + bindSingleton(overrides = true) { TestCIServer } + val defaults = ConfigurationStorage.instance().defaults.toMutableMap().apply { + put("LOG_ENVIRONMENT_VARIABLES", (!System.getenv("CI").toBoolean()).toString()) + } + + bindSingleton(overrides = true) { + ConfigurationStorage(this, defaults) + } + } + } + + @BeforeEach + fun setUpTest() { + // prep test data - restore readme contents + prepTestData(false) + } + + @Test + fun `Make changes button leads to UPDATE with specific changes flow`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "qdoc", "updateFlow") + ) + ).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) + + val result = executePuppeteerScript(updateReadmeSpecificChangesMakeChangesFlowTestScript) + + if (result.contains("Error: Test Failed")) { + println("result: $result") + } + + assertTrue(result.contains("Test Successful")) + assertFalse(result.contains("Error: Test Failed")) + } + } + + @Test + fun `UpdateReadme with specific changes returns an updated Readme`() { + val testCase = TestCase( + IdeProductProvider.IC, + LocalProjectInfo( + Paths.get("tstData", "qdoc", "updateFlow") + ) + ).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) + + val result = executePuppeteerScript(updateReadmeSpecificChangesTestScript) + + if (result.contains("Error: Test Failed")) { + println("result: $result") + } + + val readmePath = Paths.get("tstData", "qdoc", "updateFlow", "README.md") + val readme = File(readmePath.toUri()) + assertTrue(readme.exists()) + assertTrue(readme.readText().contains("Installation", ignoreCase = true)) + } + } + + companion object { + @JvmStatic + @AfterAll + fun clearAwsXml() { + clearAwsXmlFile() + } + + @JvmStatic + @BeforeAll + fun setUpTestClass() { + // Setup test environment + setupTestEnvironment() + } + } +} diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/README.md b/ui-tests-starter/tstData/qdoc/updateFlow/README.md new file mode 100644 index 00000000000..7cec42a23d0 --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/README.md @@ -0,0 +1,108 @@ +# Spring Boot REST API for Simple Data Management + +This Spring Boot application provides a lightweight REST API for managing data items with a simple key-value storage mechanism. It offers a clean and efficient way to store and retrieve data items through RESTful endpoints with built-in monitoring capabilities via Spring Actuator. + +The application implements a RESTful service with in-memory storage, making it ideal for prototyping, testing, or scenarios requiring temporary data persistence. It features comprehensive logging configuration, health monitoring through Spring Actuator, and follows Spring Boot best practices for configuration management. The service is built using Java 17 and managed with Gradle, ensuring modern Java features and reliable dependency management. + +## Repository Structure +``` +. +├── build.gradle # Gradle build configuration with Spring Boot dependencies +├── config/ +│ └── application-local.yml # Local environment configuration (port, logging, app name) +└── src/ + └── com/example/ + ├── App.java # Main application entry point with Spring Boot configuration + ├── controller/ + │ └── SampleController.java # REST endpoints for data management + └── model/ + └── DataItem.java # Data model class for storing items +``` + +## Usage Instructions + +### Prerequisites +- Java Development Kit (JDK) 17 or higher +- Gradle 8.x (or use the included Gradle wrapper) + +2. The application will be available at `http://localhost:8080` + +### More Detailed Examples + +#### Storing a Data Item +```bash +curl -X PUT http://localhost:8080/api/data/123 \ + -H "Content-Type: application/json" \ + -d '{"content": "Sample content"}' +``` + +Expected response: +```json +{ + "id": "123", + "content": "Sample content" +} +``` + +#### Retrieving a Data Item +```bash +curl http://localhost:8080/api/data/123 +``` + +Expected response: +```json +{ + "id": "123", + "content": "Sample content" +} +``` + +### Troubleshooting + +#### Common Issues + +1. Application fails to start +- **Problem**: Port 8080 already in use +- **Solution**: Modify the port in `config/application-local.yml`: +```yaml +server: + port: 8081 +``` + +2. Logging issues +- **Problem**: Insufficient logging information +- **Solution**: Adjust logging levels in `application-local.yml`: +```yaml +logging: + level: + com.example: DEBUG +``` + +#### Debugging +- Enable debug logging by adding `--debug` flag: +```bash +./gradlew bootRun --debug +``` +- View logs in console output +- Application logs are written to standard output and can be redirected to a file: +```bash +./gradlew bootRun > application.log +``` + +## Data Flow + +The application implements a simple data flow where REST requests are processed through controllers and stored in an in-memory map structure. + +```ascii +Client Request → REST Controller → In-Memory Storage + ↑ | + └────────────────────────────────────┘ + Response with stored data +``` + +Key component interactions: +1. REST requests are received by `SampleController` +2. Controller methods handle GET and PUT operations +3. Data is stored in an in-memory HashMap within the controller +4. Responses are wrapped in Spring's ResponseEntity for proper HTTP status codes +5. Spring Boot handles JSON serialization/deserialization automatically diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/build.gradle b/ui-tests-starter/tstData/qdoc/updateFlow/build.gradle new file mode 100644 index 00000000000..07aaeeb9d9f --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' + id 'java' +} + +group = 'com.sample.qdoc' +version = '1.0-SNAPSHOT' +sourceCompatibility = '17' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +test { + useJUnitPlatform() +} diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/config/application-local.yml b/ui-tests-starter/tstData/qdoc/updateFlow/config/application-local.yml new file mode 100644 index 00000000000..19631961e33 --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/config/application-local.yml @@ -0,0 +1,11 @@ +server: + port: 8080 + +spring: + application: + name: sample-rest-app + +logging: + level: + root: INFO + com.example: DEBUG diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/App.java b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/App.java new file mode 100644 index 00000000000..07f0371c403 --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/App.java @@ -0,0 +1,11 @@ +package com.sample.qdoc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/HealthController.java b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/HealthController.java new file mode 100644 index 00000000000..c22644eee7e --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/HealthController.java @@ -0,0 +1,36 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.sample.qdoc.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class HealthController { + + @GetMapping("/status") + public ResponseEntity> getStatus() { + Map status = new HashMap<>(); + status.put("timestamp", LocalDateTime.now()); + status.put("service", "sample-rest-app"); + status.put("status", "running"); + + return ResponseEntity.ok(status); + } + + @GetMapping("/health") + public ResponseEntity> healthCheck() { + Map health = new HashMap<>(); + health.put("status", "UP"); + health.put("message", "Service is healthy"); + + return ResponseEntity.ok(health); + } +} diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/SampleController.java b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/SampleController.java new file mode 100644 index 00000000000..a2f00e40d1b --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/controller/SampleController.java @@ -0,0 +1,31 @@ +package com.sample.qdoc.controller; + +import com.example.model.DataItem; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class SampleController { + + private final Map dataStore = new HashMap<>(); + + @GetMapping("/data/{id}") + public ResponseEntity getData(@PathVariable String id) { + DataItem item = dataStore.get(id); + if (item != null) { + return ResponseEntity.ok(item); + } + return ResponseEntity.notFound().build(); + } + + @PutMapping("/data/{id}") + public ResponseEntity putData(@PathVariable String id, @RequestBody DataItem item) { + item.setId(id); + dataStore.put(id, item); + return ResponseEntity.ok(item); + } +} diff --git a/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/model/DataItem.java b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/model/DataItem.java new file mode 100644 index 00000000000..ab7415b7023 --- /dev/null +++ b/ui-tests-starter/tstData/qdoc/updateFlow/src/com/sample/qdoc/model/DataItem.java @@ -0,0 +1,23 @@ +package com.sample.qdoc.model; + +public class DataItem { + private String id; + private String content; + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +}