From 17e48a6d5a019051b071393148a5b8d878f03eb9 Mon Sep 17 00:00:00 2001 From: Anna Garcia <110810707+garcanam@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:48:13 +0100 Subject: [PATCH] Mobile testing enablement adding Appium & Sauce Labs in e2e-spock-geb quickstarter (#1083) Mobile testing enablement adding Appium & Sauce Labs in e2e-spock-geb quickstarter --- CHANGELOG.md | 1 + .../quickstarters/pages/e2e-spock-geb.adoc | 204 ++++++++++-- e2e-spock-geb/Jenkinsfile | 18 ++ e2e-spock-geb/Jenkinsfile.template | 58 +++- e2e-spock-geb/README.md | 2 +- e2e-spock-geb/files/README.md | 300 ++++++++++++++++++ e2e-spock-geb/files/build.gradle | 163 ++++++---- e2e-spock-geb/files/metadata.yml | 2 +- .../groovy/DemoAcceptanceSpec.groovy | 29 -- .../modules/DemoManualMenuModule.groovy | 21 ++ .../groovy/pages/DemoGebHomePage.groovy | 17 + .../groovy/pages/DemoTheBookOfGebPage.groovy | 8 + .../groovy/specs/DemoGebHomePageSpec.groovy | 44 +++ .../groovy/specs/DemoMobileAppSpec.groovy | 68 ++++ .../specs/DemoMobileGebHomePageSpec.groovy | 70 ++++ .../acceptance/java/DemoAcceptanceTest.java | 3 +- .../groovy/DemoInstallationSpec.groovy | 3 +- .../java/DemoInstallationTest.java | 3 +- .../groovy/DemoIntegrationSpec.groovy | 7 +- .../integration/java/DemoIntegrationTest.java | 3 +- .../files/src/test/resources/GebConfig.groovy | 105 ++++-- .../src/test/resources/SpecHelper.groovy | 89 ------ .../src/test/resources/application.properties | 2 +- .../resources/helpers/Environments.groovy | 9 + .../test/resources/helpers/SpecHelper.groovy | 175 ++++++++++ 25 files changed, 1149 insertions(+), 255 deletions(-) create mode 100644 e2e-spock-geb/files/README.md delete mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/DemoAcceptanceSpec.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/modules/DemoManualMenuModule.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoGebHomePage.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoTheBookOfGebPage.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoGebHomePageSpec.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileAppSpec.groovy create mode 100644 e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileGebHomePageSpec.groovy delete mode 100644 e2e-spock-geb/files/src/test/resources/SpecHelper.groovy create mode 100644 e2e-spock-geb/files/src/test/resources/helpers/Environments.groovy create mode 100644 e2e-spock-geb/files/src/test/resources/helpers/SpecHelper.groovy diff --git a/CHANGELOG.md b/CHANGELOG.md index c252739ac..52173fa4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Removal of deprecated versions ([#1068](https://github.com/opendevstack/ods-quickstarters/issues/1068)) - Generate PDF report for cypress and improved environment management ([#1079](https://github.com/opendevstack/ods-quickstarters/pull/1079)) - Change PDF report zip file name in Cypress Quickstarter ([#1082](https://github.com/opendevstack/ods-quickstarters/pull/1082)) +- Mobile testing enablement adding Appium & Sauce Labs in e2e-spock-geb quickstarter ([#1083](https://github.com/opendevstack/ods-quickstarters/pull/1083)) ### Added diff --git a/docs/modules/quickstarters/pages/e2e-spock-geb.adoc b/docs/modules/quickstarters/pages/e2e-spock-geb.adoc index 53d8706fc..2a6a645cd 100644 --- a/docs/modules/quickstarters/pages/e2e-spock-geb.adoc +++ b/docs/modules/quickstarters/pages/e2e-spock-geb.adoc @@ -1,10 +1,10 @@ -= End-to-end tests with Spock, Geb and Unirest (e2e-spock-geb) += End-to-end tests with Spock, Geb, Unirest, and Appium (e2e-spock-geb) -spock, geb and unirest e2e testing quickstarter project +Spock, Geb, Unirest, and Appium e2e testing quickstarter project == Purpose of this quickstarter -This is a spock, geb and unirest e2e testing project quickstarter with basic setup for https://jenkins.io/[Jenkins], https://www.sonarqube.org/[SonarQube] and https://gradle.org/[Gradle]. +This is a Spock, Geb, Unirest, and Appium e2e testing project quickstarter with basic setup for https://jenkins.io/[Jenkins], https://www.sonarqube.org/[SonarQube], https://gradle.org/[Gradle], and https://appium.io/[Appium]. == What files / architecture is generated? @@ -18,7 +18,15 @@ This is a spock, geb and unirest e2e testing project quickstarter with basic set │ ├── test │ │ └── acceptance │ │ │ └── groovy -│ │ │ │ └── DemoAcceptanceSpec.groovy +│ │ │ │ └── modules +│ │ │ │ └── DemoManualMenuModule.groovy +│ │ │ │ └── pages +│ │ │ │ └── DemoGebHomePage.groovy +│ │ │ │ └── DemoTheBookOfGebPage.groovy +│ │ │ │ └── specs +│ │ │ │ └── DemoGebHomePageSpec.groovy +│ │ │ │ └── DemoMobileAppSpec.groovy +│ │ │ │ └── DemoMobileGebHomePageSpec.groovy │ │ │ └── java │ │ │ └── DemoAcceptanceTest.java │ │ └── installation @@ -30,11 +38,13 @@ This is a spock, geb and unirest e2e testing project quickstarter with basic set │ │ │ └── groovy │ │ │ │ └── DemoIntegrationSpec.groovy │ │ │ └── java -│ │ | └── DemoIntegrationTest.java +│ │ │ └── DemoIntegrationTest.java │ │ └── resources -│ │ └── application.properties -│ │ └── GebConfig.groovy -│ │ └── SpecHelper.groovy +│ │ │ └── helpers +│ │ │ └── Environments.groovy +│ │ │ └── SpecHelper.groovy +│ │ │ └── application.properties +│ │ │ └── GebConfig.groovy ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar @@ -77,19 +87,142 @@ no_nexus=true Run `gradlew -v` to verify the installed version of gradle wrapper. -== Frameworks used +=== Environments Configuration -This project is generated by https://gradle.org/[Gradle] +The environments used in the tests are defined in the `build.gradle` file. You can find the environments under the `ext` block as project properties. These environments are accessed via system properties in the Groovy classes. Here is an example of how the environments are defined: +``` +ext { +... + environments = [ + DESKTOP: "desktop", + MOBILE_BROWSER: "mobile_browser", + MOBILE_APP: "mobile_app" + ] +} +... +tasks.withType(Test) { + systemProperty 'environments.desktop', environments.DESKTOP + systemProperty 'environments.mobile_browser', environments.MOBILE_BROWSER + systemProperty 'environments.mobile_app', environments.MOBILE_APP +} +``` -******* http://spockframework.org/[spock] +Additionally, there is a class called `Environments.groovy` that facilitates access to these environments from the Groovy code. The class is located in the `src/test/resources/helpers` directory. +``` +package helpers + +class Environments { + // Define environment constants using system properties + // Check the build.gradle file for the defined properties + static final String DESKTOP = System.getProperty('environments.desktop') + static final String MOBILE_BROWSER = System.getProperty('environments.mobile_browser') + static final String MOBILE_APP = System.getProperty('environments.mobile_app') +} +``` -******* https://gebish.org/[geb] +For each environment, we have a driver configured in the GebConfig.groovy file: +``` + environments { + + // Configuration for desktop environment using HtmlUnitDriver + "${Environments.DESKTOP}" { + driver = { + HtmlUnitDriver driver = new HtmlUnitDriver(BrowserVersion.BEST_SUPPORTED, true) { + ... + } + } + } + + // Configuration for mobile browser environment using AndroidDriver + "${Environments.MOBILE_BROWSER}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + ... + MutableCapabilities sauceOptions = new MutableCapabilities() + ... + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + AndroidDriver driver = new AndroidDriver(url, caps) + return driver + } + } + + // Configuration for mobile app environment using IOSDriver + "${Environments.MOBILE_APP}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + ... + MutableCapabilities sauceOptions = new MutableCapabilities() + ... + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + IOSDriver driver = new IOSDriver(url, caps) + return driver + } + } + } +``` -******* http://unirest.io/[unirest] +In the build.gradle file, there is also a filter by environment and tags: +``` + // Specify the tags to include/exclude for each environment + // Please, adjust the tags according to your project needs + useJUnitPlatform { + switch (env) { + case environments.DESKTOP: + includeTags 'test_desktop' + break + case environments.MOBILE_BROWSER: + includeTags 'test_mobile_browser' + break + case environments.MOBILE_APP: + includeTags 'test_mobile_app' + break + } + } +``` + +== Sauce Labs Integration + +Sauce Labs is a cloud-based platform that provides comprehensive testing solutions for web and mobile applications. It allows you to run tests on a wide range of real devices and emulators/simulators, ensuring your applications work seamlessly across different environments. -## Usage - how do you start after you provisioned this quickstarter +This template is prepared to work with Sauce Labs virtual devices, allowing you to perform all mobile tests on these virtual devices. -* Run command `gradlew test` in project directory to execute the end-to-end tests via spock/geb against the Google Home page and demo jUnit 5 tests. +=== Key Features of Sauce Labs + +* **Real Device Cloud**: Access to thousands of real Android and iOS devices for manual and automated testing. +* **Emulators and Simulators**: Cost-effective and scalable testing on virtual devices. +* **Cross-Browser Testing**: Ensure compatibility across various browser and OS combinations. +* **Error Monitoring and Reporting**: Capture and resolve application errors quickly with detailed insights. +* **CI/CD Integration**: Seamlessly integrate with your continuous integration and delivery pipelines. + +=== Credentials for Sauce Labs + +To execute tests on Sauce Labs, you need Sauce Labs credentials. These credentials are stored in a secret called sauce-labs-user-access-key, which by default is created with "changeme" values for both username and password. Users will need to update these values with their actual Sauce Labs credentials. + +In the Jenkinsfile, the credentials are retrieved as follows: +``` + // Use credentials for SauceLabs authentication + // You can remove this block if you are not using SauceLabs + withCredentials([ + usernamePassword(credentialsId: "${context.projectId}-cd-sauce-labs-user-access-key", passwordVariable: 'SAUCE_LABS_ACCESS_KEY', usernameVariable: 'SAUCE_LABS_USERNAME'), + ]) { + ... + } +``` + +In the GebConfig.groovy file, these credentials are used to configure the drivers: +``` + // Get SauceLabs environment variables for configuring iOS device + def sauceLabsUsername = System.getenv('SAUCE_LABS_USERNAME') + def sauceLabsAccessKey = System.getenv('SAUCE_LABS_ACCESS_KEY') +``` + +This setup ensures that your tests can authenticate with Sauce Labs and run on the specified virtual devices. + +== Usage - how do you start after you provisioned this quickstarter + +* Run command `gradlew test` in project directory to execute the end-to-end tests via spock/geb against the demo pages and demo jUnit 5 tests. You will see the results inside a new folder 'build' in project directory. @@ -97,27 +230,42 @@ You will see the results inside a new folder 'build' in project directory. . └── build └── test-results - ├── acceptance-groovy - │ └── TEST-DemoAcceptance.xml - ├── acceptance-java - │ └── TEST-DemoAcceptanceTest.xml - ├── installation-groovy - │ └── TEST-DemoInstallation.xml - ├── installation-java + ├── acceptance-groovy-desktop + ├── acceptance-java-desktop + │ │── TEST-DemoAcceptanceTest.xml + │ │── TEST-specs.DemoGebHomePageSpec.xml + │ │── TEST-specs.DemoMobileAppSpec.xml + │ └── TEST-specs.DemoMobileGebHomePageSpec.xml + ├── installation-groovy-desktop + ├── installation-java-desktop + │ │── TEST-DemoInstallationSpec.xml │ └── TEST-DemoInstallationTest.xml - ├── integration-groovy - │ └── TEST-DemoIntegration.xml - └── integration-java + ├── integration-groovy-desktop + └── integration-java-desktop + │── TEST-DemoIntegrationSpec.xml └── TEST-DemoIntegrationTest.xml ---- +== Frameworks used + +This project is generated by https://gradle.org/[Gradle] + +******* http://spockframework.org/[spock] + +******* https://gebish.org/[geb] + +******* http://unirest.io/[unirest] + +******* https://appium.io/[apium] + +******* https://saucelabs.com/[Sauce Labs] ## Customization - how do you start to configure your test * You can see how a Java Junit 5 test are developed showing the Demo*Test.java files. * You can see how a Groovy Spock/Geb test are developed showing the Demo*Spec.groovy files. ** The url to test with Geb is configured in the property `config.application.url` inside `application.properties` -** Inside `GebConfig.groovy` you could change the default navigator (CHROME) +** Inside `GebConfig.groovy` you could see some environments with different drivers defined and a default browser. You could configure or change them as you need. == How this quickstarter is built through jenkins @@ -134,8 +282,6 @@ In Jenkinsfile.template, there is the following stage: All the results are stashed and published through Jenkins jUnit publisher. -include::partials$secret-scanning-with-gitleaks.adoc - == Builder agent used This quickstarter uses the @@ -143,4 +289,4 @@ https://github.com/opendevstack/ods-quickstarters/tree/master/common/jenkins-age == Known limitations -NA +NA \ No newline at end of file diff --git a/e2e-spock-geb/Jenkinsfile b/e2e-spock-geb/Jenkinsfile index 5ab91e95d..8516b4f52 100644 --- a/e2e-spock-geb/Jenkinsfile +++ b/e2e-spock-geb/Jenkinsfile @@ -21,6 +21,24 @@ odsQuickstarterPipeline( odsQuickstarterStageCopyFiles(context) odsQuickstarterStageRenderJenkinsfile(context) + + createSauceLabsOpenshiftSecret(context) odsQuickstarterStageRenderSonarProperties(context) } + +def createSauceLabsOpenshiftSecret(def context) { + stage('Create Credentials') { + def project = context.projectId + "-cd" + def secretExists = sh(script: "oc get secret sauce-labs-user-access-key -n $project", returnStatus: true) == 0 + if (!secretExists) { + sh """ + oc create secret generic sauce-labs-user-access-key -n $project \ + --from-literal=username=changeme \ + --from-literal=password=changeme \ + --type="kubernetes.io/basic-auth" && \ + oc label secret sauce-labs-user-access-key -n $project credential.sync.jenkins.openshift.io=true + """ + } + } +} \ No newline at end of file diff --git a/e2e-spock-geb/Jenkinsfile.template b/e2e-spock-geb/Jenkinsfile.template index 7e363d92c..0cce0f820 100644 --- a/e2e-spock-geb/Jenkinsfile.template +++ b/e2e-spock-geb/Jenkinsfile.template @@ -1,7 +1,9 @@ // See https://www.opendevstack.org/ods-documentation/ for usage and customization. +// Import the shared Jenkins library @Library('ods-jenkins-shared-library@@shared_library_ref@') _ +// Define the Jenkins pipeline odsComponentPipeline( imageStreamTag: '@ods_namespace@/jenkins-agent-jdk:@agent_image_tag@', branchToEnvironmentMapping: [ @@ -13,6 +15,7 @@ odsComponentPipeline( odsComponentStageScanWithSonar(context) } +// Define the test stage def stageTest(def context) { def javaOpts = "-Xmx512m" def gradleTestOpts = "-Xmx128m" @@ -21,16 +24,51 @@ def stageTest(def context) { springBootEnv = 'dev' } - stage('Integration Test') { - sh (script: "chmod a+x gradle*", label : "allow gradle to execute") - withEnv(["TAGVERSION=${context.tagversion}", "NEXUS_HOST=${context.nexusHost}", "NEXUS_USERNAME=${context.nexusUsername}", "NEXUS_PASSWORD=${context.nexusPassword}", "JAVA_OPTS=${javaOpts}","GRADLE_TEST_OPTS=${gradleTestOpts}","ENVIRONMENT=${springBootEnv}","OPENSHIFT_PROJECT=${context.targetProject}","OPENSHIFT_APP_DOMAIN=${context.getOpenshiftApplicationDomain()}"]) { - def status = sh(script: "./gradlew clean test --stacktrace --no-daemon", returnStatus: true) - junit(testResults:"build/test-results/installation*/*.xml, build/test-results/integration*/*.xml, build/test-results/acceptance*/*.xml", allowEmptyResults:true) - stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation*/*.xml', allowEmpty: true) - stash(name: "integration-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/integration*/*.xml', allowEmpty: true) - stash(name: "acceptance-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/acceptance*/*.xml', allowEmpty: true) - if (status != 0) { - error "Executing tests failed!" + stage('Functional Test') { + sh (script: "chmod a+x gradle*", label : "allow gradle to execute") + withEnv([ + "TAGVERSION=${context.tagversion}", + "NEXUS_HOST=${context.nexusHost}", + "NEXUS_USERNAME=${context.nexusUsername}", + "NEXUS_PASSWORD=${context.nexusPassword}", + "JAVA_OPTS=${javaOpts}", + "GRADLE_TEST_OPTS=${gradleTestOpts}", + "ENVIRONMENT=${springBootEnv}", + "OPENSHIFT_PROJECT=${context.targetProject}", + "OPENSHIFT_APP_DOMAIN=${context.getOpenshiftApplicationDomain()}" + ]) { + // Use credentials for SauceLabs authentication + // You can remove this block if you are not using SauceLabs + withCredentials([ + usernamePassword(credentialsId: "${context.projectId}-cd-sauce-labs-user-access-key", passwordVariable: 'SAUCE_LABS_ACCESS_KEY', usernameVariable: 'SAUCE_LABS_USERNAME'), + ]) { + // Note: Testing in the production environment is not enabled by default as it can lead to unintended consequences, + // including potential downtime, data corruption, or exposure of sensitive information. + // This block is designed to skip acceptance and integration tests in the production environment to avoid these risks. + // If you choose to enable these tests in production take all necessary precautions. This means verifying your + // preconditions, database access, fake data, API calls, etc. + // Remember that any test case in the installation folder will be executed in production. + def status + if (context.environment == 'prod') { + status = sh(script: './gradlew clean testProd --stacktrace --no-daemon', returnStatus: true) + junit(testResults:"build/test-results/installation*/*.xml", allowEmptyResults:true) + stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation*/*.xml', allowEmpty: true) + } else { + status = sh(script: './gradlew clean test --stacktrace --no-daemon', returnStatus: true) + junit(testResults:"build/test-results/installation*/*.xml, build/test-results/integration*/*.xml, build/test-results/acceptance*/*.xml", allowEmptyResults:true) + stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation*/*.xml', allowEmpty: true) + stash(name: "integration-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/integration*/*.xml', allowEmpty: true) + stash(name: "acceptance-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/acceptance*/*.xml', allowEmpty: true) + } + + // Handle the test status + switch (status) { + case 0: + echo "All tests passed successfully" + break + default: + unstable "Some tests have failed or encountered errors. Please check the logs for more details." + } } } } diff --git a/e2e-spock-geb/README.md b/e2e-spock-geb/README.md index d84b7b0db..c7249c0f2 100644 --- a/e2e-spock-geb/README.md +++ b/e2e-spock-geb/README.md @@ -2,6 +2,6 @@ Documentation is located in our [official documentation](https://www.opendevstack.org/ods-documentation/opendevstack/3.x/quickstarters/e2e-spock-geb.html) -Please update documentation in the [antora page directory](https://github.com/opendevstack/ods-project-quickstarters/tree/master/docs/modules/ROOT/pages) +Please update documentation in the [antora page directory](https://github.com/opendevstack/ods-quickstarters/tree/master/docs/modules/ROOT/pages) Tested thru [automated tests](../tests/e2e-spock-geb) diff --git a/e2e-spock-geb/files/README.md b/e2e-spock-geb/files/README.md new file mode 100644 index 000000000..e4f8cbe12 --- /dev/null +++ b/e2e-spock-geb/files/README.md @@ -0,0 +1,300 @@ +# Spock & Geb end-to-end tests + +This end-to-end testing project was generated from the *e2e-spock-geb* ODS quickstarter. + +## Description + +This QuickStarter integrates three powerful tools: Spock, Geb, and Appium, each serving a unique purpose in the realm of testing and automation. + +Spock is a dynamic and comprehensive testing and specification framework designed specifically for Java and Groovy. +It allows for clear and concise testing, making it easier to write and understand tests. + +Geb, on the other hand, is a robust solution for browser automation. +It caters to functional, web, and acceptance testing, providing a seamless way to automate browser interactions. + +Appium is a versatile platform for automating mobile testing. +It supports a wide range of languages and testing frameworks, making it a go-to solution for mobile application testing. + +The purpose of this QuickStarter is to provide a customized, integrated environment that leverages the strengths of these three tools. +It simplifies the setup process, allowing you to focus on writing and executing tests. +It's designed to accelerate your testing efforts, improve efficiency, and ultimately, help deliver high-quality software. + +## Project Organization + +The project is structured as follows: + +The `src/test` directory contains all the tests, which are further divided into `acceptance`, `installation`, and `integration` subdirectories. +These directories accommodate tests written in both `groovy` and `java`. The classes are organized in a structured manner, +with designated spaces for `modules`, `pages`, and `specs`. + +In addition to these, within the src/test directory, there is a resources section. Here, you will find important configuration files such as `application.properties` and `GebConfig.groovy`. +These resources are crucial for the configuration and efficient operation of the project. + +Also within the `src/test` directory, you will find a `helpers` folder. This folder contains the `SpecHelper.groovy` file, which includes several functions designed to assist you in your development process, such as functions for capturing evidence, and the `Environments.groovy` to define the different environments in which the tests can be executed. + +## Working with GebConfig.groovy + +The `GebConfig.groovy` file is a configuration file used by Geb for browser automation. It allows you to define various environments and set up different drivers for testing. + +### Application Properties +Loading Application Properties: This section loads the application properties using the SpecHelper class. +``` +// Load application.properties +def properties = new SpecHelper().getApplicationProperties() +``` + +### Environments + +This section defines different environments such as DESKTOP, MOBILE_BROWSER, and MOBILE_APP. Each environment has its own driver setup. + +These environments are dedicated to browser testing: + DESKTOP - This environment uses the HtmlUnitDriver for headless browser testing. + MOBILE_BROWSER - This environment uses the AndroidDriver for testing on mobile browsers. +And this environment is dedicated to mobile app testing: + MOBILE_APP - This environment uses the IOSDriver for testing on iOS mobile applications. + +* Desktop Environment - The DESKTOP environment is configured to use the HtmlUnitDriver: + ``` + "${Environments.DESKTOP}" { + driver = { + HtmlUnitDriver driver = new HtmlUnitDriver(BrowserVersion.BEST_SUPPORTED, true) { + @Override + protected WebClient newWebClient(BrowserVersion version) { + WebClient webClient = super.newWebClient(version) + webClient.getOptions().setThrowExceptionOnScriptError(false) + webClient.getOptions().setCssEnabled(false) + return webClient + } + } + def env = System.getenv() + if(env.HTTP_PROXY) { + Proxy proxy = new Proxy(); + URL url = new URL(env.HTTP_PROXY); + proxy.setHttpProxy("${url.getHost()}:${url.getPort()}"); + proxy.setNoProxy(env.NO_PROXY) + driver.setProxySettings(proxy); + } + return driver + } + } + ``` +* Mobile Browser Environment - The MOBILE_BROWSER environment is configured to use the AndroidDriver: + ``` + "${Environments.MOBILE_BROWSER}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + caps.setCapability("platformName", "Android") + caps.setCapability("browserName", "Chrome") + caps.setCapability("appium:deviceName", "Google Pixel 7a GoogleAPI Emulator") + caps.setCapability("appium:platformVersion", "13.0") + caps.setCapability("appium:automationName", "UiAutomator2") + MutableCapabilities sauceOptions = new MutableCapabilities() + sauceOptions.setCapability("appiumVersion", "2.11.0") + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) + sauceOptions.setCapability("build", "") + sauceOptions.setCapability("name", "") + sauceOptions.setCapability("deviceOrientation", "PORTRAIT") + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + AndroidDriver driver = new AndroidDriver(url, caps) + return driver + } + } + ``` +* Mobile App Environment - The MOBILE_APP environment is configured to use the IOSDriver: + ``` + "${Environments.MOBILE_APP}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + caps.setCapability("platformName", "iOS") + caps.setCapability("appium:app", "storage:filename=SauceLabs-Demo-App.Simulator.XCUITest.zip") + caps.setCapability("appium:deviceName", "iPhone Simulator") + caps.setCapability("appium:platformVersion", "17.0") + caps.setCapability("appium:automationName", "XCUITest") + MutableCapabilities sauceOptions = new MutableCapabilities() + sauceOptions.setCapability("appiumVersion", "2.1.3") + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) + sauceOptions.setCapability("build", "") + sauceOptions.setCapability("name", "") + sauceOptions.setCapability("deviceOrientation", "PORTRAIT") + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + IOSDriver driver = new IOSDriver(url, caps) + return driver + } + } + ``` +### SauceLabs Integration +Sauce Labs is a cloud-based platform that provides comprehensive testing solutions for web and mobile applications. It allows you to run tests on a wide range of real devices and emulators/simulators, ensuring your applications work seamlessly across different environments. + +This template is prepared to work with Sauce Labs virtual devices, allowing you to perform all mobile tests on these virtual devices. + +#### Credentials for Sauce Labs +To execute tests on Sauce Labs, you need Sauce Labs credentials. These credentials are stored in a secret called sauce-labs-user-access-key, which by default is created with "changeme" values for both username and password. Users will need to update these values with their actual Sauce Labs credentials. + +In the Jenkinsfile, the credentials are retrieved as follows: +``` + // Use credentials for SauceLabs authentication + // You can remove this block if you are not using SauceLabs + withCredentials([ + usernamePassword(credentialsId: "${context.projectId}-cd-sauce-labs-user-access-key", passwordVariable: 'SAUCE_LABS_ACCESS_KEY', usernameVariable: 'SAUCE_LABS_USERNAME'), + ]) { + ... + } +``` + +In the GebConfig.groovy file, these credentials are used to configure the mobile drivers: +``` + // Get SauceLabs environment variables for configuring iOS device + def sauceLabsUsername = System.getenv('SAUCE_LABS_USERNAME') + def sauceLabsAccessKey = System.getenv('SAUCE_LABS_ACCESS_KEY') + + ... + + // Configuration for mobile browser environment using AndroidDriver + "${Environments.MOBILE_BROWSER}" { + driver = { + ... + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) + + ... + + // Configuration for mobile app environment using IOSDriver + "${Environments.MOBILE_APP}" { + driver = { + ... + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) +``` + +This setup ensures that your tests can authenticate with Sauce Labs and run on the specified virtual devices. + +### Base URL + +This refers to the root URL of the application under test. Please modify config.application.url in the 'application.properties' file, located within 'src\test\resources'. +```baseUrl = properties."config.application.url"``` + +### Reports Directory + +This is the directory where the Geb reports will be stored. Please modify config.reports.dir in the 'application.properties' file, located within 'src\test\resources'. +```reportsDir = new File(properties."config.reports.dir")``` + +## Test example + +This project is structured with different sections for `pages`, `modules`, and `specs`. Here is a walkthrough of how they interact: + +### Pages Section +In this section, we have the `DemoGebHomePage` class which defines the home page, https://gebish.org. +This class includes the title of the page and some of its content, specifically the `manuals` menu. +There is a second web page defined called DemoTheBookOfGebPage, which can be accessed through the 'manuals' menu. +``` + package pages + + import geb.Page + import modules.DemoManualMenuModule + + class DemoGebHomePage extends Page { + // URL of the Geb home page + static url = "https://gebish.org" + + // Condition to verify that the browser is at the correct page + static at = { title == "Geb - Very Groovy Browser Automation" } + + static content = { + // Define the manuals menu module + manualsMenu { module(DemoManualMenuModule) } + } + } +``` + +### Modules Section +The `manuals` menu from the home page is defined as a module in this section. +``` + package modules + + class DemoManualMenuModule extends geb.Module { + static content = { + // Define the toggle element for the manuals menu + toggle { $("div.menu a.manuals") } + + // Define the container for the links in the manuals menu + linksContainer { $("#manuals-menu") } + + // Define the links within the links container + links { linksContainer.find("a") } + } + + // Method to open the manuals menu + void open() { + toggle.click() + // Wait until the links container is no longer animating + waitFor { !linksContainer.hasClass("animating") } + } + } +``` + +### Specs Section + +#### Gebish.org example - for DESKTOP + +The test that utilizes these pages and modules is located in the `specs` section and is called `DemoGebHomePageSpec`. + +**Setup** - In the `DemoGebHomePageSpec` test, the `def setupSpec()` method is used to check the environment and skips tests if it is not DESKTOP. + +**Test Case** - The test case `can access The Book of Geb via homepage` is defined in this test. This test case simply accesses the `gebHomePage` and then accesses the `theBookOfGebPage` by opening the `manualsMenu` module. + +**Evidence Collection** - During the process of executing this test, multiple pieces of evidence are collected. +``` + // Print evidence of the introduction header element + SpecHelper.printEvidenceForPageElement(this, 1, $("#introduction"), "Introduction header") + + // Print evidence of the first and second paragraphs + SpecHelper.printEvidenceForPageElements(this, 1, + [ + [ 'fragment' : $("#content > div:nth-child(2) > div > div:nth-child(1)"), 'description' : '1st paragraph'], + [ 'fragment' : $("#content > div:nth-child(2) > div > div:nth-child(2)"), 'description' : '2nd paragraph'] + ] + ) +``` + +#### Gebish.org example - for MOBILE_BROWSER +The test that utilizes these elements is located in the `specs` section and is called `DemoMobileGebHomePageSpec`. + +**Setup** - In the `DemoMobileGebHomePageSpec` test, the `def setupSpec()` method is used to check the environment and skips tests if it is not MOBILE_BROWSER, and initialize the Appium driver. + +**Test Case** - The test case ` verify geb home page and documentation navigation` is defined in this test. This test case navigates to the Geb home page and then accesses the Documentation page. + +**Evidence Collection** - During the process of executing this test, evidence is collected. +``` + // Print evidence for the Documentation button + SpecHelper.printEvidenceForWebElement(this, 1, documentationButton, "Documentation Button Evidence") +``` + +#### My Demo App Sauce Labs example - for MOBILE_APP +The test that utilizes these elements is located in the `specs` section and is called `DemoMobileGebHomePageSpec`. + +**Setup** - In the `DemoMobileAppSpec` test, the `def setupSpec()` method is used to check the environment and skips tests if it is not MOBILE_APP, and initialize the Appium driver + +**Test Case** - The test case `check elements in the first page of the app` is defined in this test. This test case verifies the presence of a specific element and interacts with it. + +**Evidence Collection** - During the process of executing this test, evidence is collected for the specific element. + +``` + // Print evidence for the specific element + SpecHelper.printEvidenceForWebElement(this, 1, specificElements[0], "Cart-tab-item Element Evidence") +``` + +## Running end-to-end tests + +Run `gradlew test` to execute all end-to-end tests against the test instance of the front end. + +## Links + +* [gradle](https://gradle.org/) +* [spock](http://spockframework.org/) +* [geb](https://gebish.org/) +* [unirest](http://unirest.io/) +* [apium](https://appium.io/) diff --git a/e2e-spock-geb/files/build.gradle b/e2e-spock-geb/files/build.gradle index 733acd9e0..51339f518 100644 --- a/e2e-spock-geb/files/build.gradle +++ b/e2e-spock-geb/files/build.gradle @@ -19,9 +19,15 @@ buildscript { } plugins { - id 'java' - id 'groovy' - id 'com.adarshr.test-logger' version "2.0.0" + id 'java' + id 'groovy' + id 'com.adarshr.test-logger' version "2.0.0" +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } } repositories { @@ -52,11 +58,21 @@ ext { junitVersion = "5.10.1" spockVersion = "2.3-groovy-4.0" gebVersion = "7.0" - seleniumVersion = "4.15.0" + seleniumVersion = "4.25.0" htmlunitVersion = "4.13.0" unirestVersion = "3.14.5" - // When a test fail we can continue or fail the stage + appiumVersion = "9.3.0" + chromeDriverVersion = "4.2.2" + edgeDriverVersion = "4.21.0" + apiGuardianAPI = "1.1.2" CONTINUE_WHEN_TEST_FAIL = true + // Define environments as project properties to be accessed via system properties in Groovy classes + // Please, adjust the environments according to your project needs + environments = [ + DESKTOP: "desktop", +// MOBILE_BROWSER: "mobile_browser", +// MOBILE_APP: "mobile_app" + ] } dependencies { @@ -65,51 +81,30 @@ dependencies { testImplementation "com.konghq:unirest-java:${unirestVersion}" testImplementation "org.gebish:geb-spock:${gebVersion}" testImplementation "org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}" - testImplementation "org.seleniumhq.selenium:htmlunit-driver:${htmlunitVersion}" testImplementation "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" + testImplementation "org.seleniumhq.selenium:selenium-java:${seleniumVersion}" + testImplementation "org.seleniumhq.selenium:htmlunit-driver:${htmlunitVersion}" + testImplementation "org.seleniumhq.selenium:selenium-chrome-driver:${chromeDriverVersion}" + testImplementation "org.seleniumhq.selenium:selenium-edge-driver:${edgeDriverVersion}" + testImplementation "io.appium:java-client:${appiumVersion}" + testImplementation "org.apiguardian:apiguardian-api:${apiGuardianAPI}" } sourceSets { - installation { - groovy { - srcDir 'src/test/installation/groovy' - } - java { - srcDir 'src/test/installation/java' - } - resources { - srcDir 'src/test/resources' - } - compileClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath - runtimeClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath - } - - integration { - groovy { - srcDir 'src/test/integration/groovy' - } - java { - srcDir 'src/test/integration/java' - } - resources { - srcDir 'src/test/resources' - } - compileClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath - runtimeClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath - } - - acceptance { - groovy { - srcDir 'src/test/acceptance/groovy' - } - java { - srcDir 'src/test/acceptance/java' - } - resources { - srcDir 'src/test/resources' + [TestExecutionPhases.INSTALLATION, TestExecutionPhases.INTEGRATION, TestExecutionPhases.ACCEPTANCE].each { phase -> + "$phase" { + groovy { + srcDir "src/test/${phase}/groovy" + } + java { + srcDir "src/test/${phase}/java" + } + resources { + srcDir 'src/test/resources' + } + compileClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath + runtimeClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath } - compileClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath - runtimeClasspath += sourceSets.main.output + sourceSets.test.output + configurations.testRuntimeClasspath } } @@ -135,15 +130,31 @@ class TestLanguages { static final String GROOVY = "groovy" } -def generateTaskName(def type, def language) { - return "${type}-${language}" +def generateTaskName(def type, def language, def env) { + return "${type}-${language}-${env}" } // Task to create in a parametrized way Tests task -def executeTest(def type, def language) { - return tasks.create(generateTaskName(type, language), Test) { - description = "Runs ${type} tests ${language}." - group = "verification" +def executeTest(def type, def language, def env) { + return tasks.create(generateTaskName(type, language, env), Test) { + description = "Runs ${type} tests ${language} in ${env} environment." + group = "verification-${type}" + + // Specify the tags to include/exclude for each environment + // Please, adjust the tags according to your project needs + useJUnitPlatform { + switch (env) { + case environments.DESKTOP: + includeTags 'test_desktop' + break +// case environments.MOBILE_BROWSER: +// includeTags 'test_mobile_browser' +// break +// case environments.MOBILE_APP: +// includeTags 'test_mobile_app' +// break + } + } // Since groovy tests are junit tests and they are run as part of java tests, // we disable here the execution of junit tests if not running for java language. @@ -156,24 +167,48 @@ def executeTest(def type, def language) { ignoreFailures = "${CONTINUE_WHEN_TEST_FAIL}" testClassesDirs = sourceSets["${type}"].output.classesDirs classpath = sourceSets["${type}"].runtimeClasspath + + systemProperty "geb.env", env + } } test { + // To create the tasks - dependsOn executeTest(TestExecutionPhases.INSTALLATION, TestLanguages.JAVA) - dependsOn executeTest(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY) - dependsOn executeTest(TestExecutionPhases.INTEGRATION, TestLanguages.JAVA) - dependsOn executeTest(TestExecutionPhases.INTEGRATION, TestLanguages.GROOVY) - dependsOn executeTest(TestExecutionPhases.ACCEPTANCE, TestLanguages.JAVA) - dependsOn executeTest(TestExecutionPhases.ACCEPTANCE, TestLanguages.GROOVY) - - // To define the order - tasks.findByName(generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.GROOVY)).mustRunAfter generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.JAVA) - tasks.findByName(generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.JAVA)).mustRunAfter generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.GROOVY) - tasks.findByName(generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.GROOVY)).mustRunAfter generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.JAVA) - tasks.findByName(generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.JAVA)).mustRunAfter generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY) - tasks.findByName(generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY)).mustRunAfter generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.JAVA) + environments.each { env -> + [TestExecutionPhases.INSTALLATION, TestExecutionPhases.INTEGRATION, TestExecutionPhases.ACCEPTANCE].each { phase -> + [TestLanguages.JAVA, TestLanguages.GROOVY].each { language -> + dependsOn executeTest(phase, language, env.value) + } + } + // To define the order + tasks.findByName(generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.GROOVY, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.JAVA, env.value) + tasks.findByName(generateTaskName(TestExecutionPhases.ACCEPTANCE, TestLanguages.JAVA, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.GROOVY, env.value) + tasks.findByName(generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.GROOVY, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.JAVA, env.value) + tasks.findByName(generateTaskName(TestExecutionPhases.INTEGRATION, TestLanguages.JAVA, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY, env.value) + tasks.findByName(generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.JAVA, env.value) + } +} + +task testProd { + description = "Runs only the installation phase tests." + group = "verification" + + // Define dependencies on installation phase tasks + environments.each { env -> + [TestLanguages.JAVA, TestLanguages.GROOVY].each { language -> + dependsOn generateTaskName(TestExecutionPhases.INSTALLATION, language, env.value) + } + // To define the order + tasks.findByName(generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.GROOVY, env.value)).mustRunAfter generateTaskName(TestExecutionPhases.INSTALLATION, TestLanguages.JAVA, env.value) + } +} + +tasks.withType(Test) { + systemProperty 'environments.desktop', environments.DESKTOP + systemProperty 'environments.mobile_browser', environments.MOBILE_BROWSER + systemProperty 'environments.mobile_app', environments.MOBILE_APP } testlogger { diff --git a/e2e-spock-geb/files/metadata.yml b/e2e-spock-geb/files/metadata.yml index c29b7a401..4e5d0ff4c 100644 --- a/e2e-spock-geb/files/metadata.yml +++ b/e2e-spock-geb/files/metadata.yml @@ -1,6 +1,6 @@ --- name: e2e-spock-geb -description: "Spock is a highly expressive testing and specification framework for Java and Groovy. Geb is a browser automation solution for functional/web/acceptance testing. Technologies: Spock 2.3-groovy-4.0, Geb 7.0, Selenium 4.15.0" +description: "Spock is a highly expressive testing and specification framework for Java and Groovy. Geb is a browser automation solution for functional/web/acceptance testing. Appium is an open-source tool for automating mobile apps. Technologies: Spock 2.3-groovy-4.0, Geb 7.0, Selenium 4.25.0, Appium 9.3.0" supplier: http://spockframework.org version: 4.x type: ods-test diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/DemoAcceptanceSpec.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/DemoAcceptanceSpec.groovy deleted file mode 100644 index 768648e2a..000000000 --- a/e2e-spock-geb/files/src/test/acceptance/groovy/DemoAcceptanceSpec.groovy +++ /dev/null @@ -1,29 +0,0 @@ -import geb.Page -import geb.spock.GebReportingSpec -import spock.lang.Stepwise - -class GitHubAcceptanceHomePage extends Page { - static url = "/opendevstack/ods-quickstarters" - static at = { title.contains("quickstarters")} -} - -@Stepwise -class DemoAcceptance extends GebReportingSpec { - - def "goes to GH ods-quickstarters"() { - given: "User goes to ods-quickstarters and checks the content" - to GitHubAcceptanceHomePage - - // print evidence of two fields (the input area and iframe content) - SpecHelper.printEvidenceForPageElement(this, 1, $("[data-content='Code']"), "code area") - SpecHelper.printEvidenceForPageElement(this, 1, $("#iframecontainer"), "rendered code area") - - // print the two evidence fields through map - SpecHelper.printEvidenceForPageElements(this, 1, - [ - [ 'fragment' : $("#textareaCode"), 'description' : 'code area'], - [ 'fragment' : $("#iframecontainer"), 'description' : 'rendered code area'] - ] - ) - } -} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/modules/DemoManualMenuModule.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/modules/DemoManualMenuModule.groovy new file mode 100644 index 000000000..f4eec56cb --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/modules/DemoManualMenuModule.groovy @@ -0,0 +1,21 @@ +package modules + +class DemoManualMenuModule extends geb.Module { + static content = { + // Define the toggle element for the manuals menu + toggle { $("div.menu a.manuals") } + + // Define the container for the links in the manuals menu + linksContainer { $("#manuals-menu") } + + // Define the links within the links container + links { linksContainer.find("a") } + } + + // Method to open the manuals menu + void open() { + toggle.click() + // Wait until the links container is no longer animating + waitFor { !linksContainer.hasClass("animating") } + } +} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoGebHomePage.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoGebHomePage.groovy new file mode 100644 index 000000000..08c6d6083 --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoGebHomePage.groovy @@ -0,0 +1,17 @@ +package pages + +import geb.Page +import modules.DemoManualMenuModule + +class DemoGebHomePage extends Page { + // URL of the Geb home page + static url = "https://gebish.org" + + // Condition to verify that the browser is at the correct page + static at = { title == "Geb - Very Groovy Browser Automation" } + + static content = { + // Define the manuals menu module + manualsMenu { module(DemoManualMenuModule) } + } +} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoTheBookOfGebPage.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoTheBookOfGebPage.groovy new file mode 100644 index 000000000..027e2d1aa --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/pages/DemoTheBookOfGebPage.groovy @@ -0,0 +1,8 @@ +package pages + +import geb.Page + +class DemoTheBookOfGebPage extends Page { + // Verify that the page title starts with "The Book Of Geb" + static at = { title.startsWith("The Book Of Geb") } +} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoGebHomePageSpec.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoGebHomePageSpec.groovy new file mode 100644 index 000000000..9dc5ad57a --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoGebHomePageSpec.groovy @@ -0,0 +1,44 @@ +package specs + +import geb.spock.GebReportingSpec +import pages.DemoGebHomePage +import pages.DemoTheBookOfGebPage +import helpers.SpecHelper +import spock.lang.Tag + +class DemoGebHomePageSpec extends GebReportingSpec { + + // Define page objects for the home page and the book page + def gebHomePage = page(DemoGebHomePage) + def theBookOfGebPage = page(DemoTheBookOfGebPage) + + // Add this @Tag to include this test in the 'test_desktop' group. + // This tag ensures the test runs in the Environment.DESKTOP configuration. + @Tag("test_desktop") + def "can access The Book of Geb via homepage"() { + given: + // Navigate to the Geb home page + to gebHomePage + + when: + // Open the manuals menu and click the first link + gebHomePage.manualsMenu.open() + gebHomePage.manualsMenu.links[0].click() + + // Print evidence of the introduction header element + SpecHelper.printEvidenceForPageElement(this, 1, $("#introduction"), "Introduction header") + + // Print evidence of the first and second paragraphs + SpecHelper.printEvidenceForPageElements(this, 1, + [ + [ 'fragment' : $("#content > div:nth-child(2) > div > div:nth-child(1)"), 'description' : '1st paragraph'], + [ 'fragment' : $("#content > div:nth-child(2) > div > div:nth-child(2)"), 'description' : '2nd paragraph'] + ] + ) + + then: + // Verify that the browser is at the book page + at theBookOfGebPage + } + +} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileAppSpec.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileAppSpec.groovy new file mode 100644 index 000000000..2997eccd7 --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileAppSpec.groovy @@ -0,0 +1,68 @@ +package specs + +import geb.spock.GebReportingSpec +import io.appium.java_client.AppiumDriver +import io.appium.java_client.AppiumBy +import org.openqa.selenium.WebElement +import spock.lang.Shared +import spock.lang.Stepwise +import spock.lang.Tag +import helpers.SpecHelper + +@Stepwise +class DemoMobileAppSpec extends GebReportingSpec { + + // Shared driver instance for the AppiumDriver + @Shared + def static driver + // Shared variable to indicate the success of the test for Sauce Labs + @Shared + def static sauceLabsResult = false + + def setupSpec() { + // Initialize the Appium driver + driver = browser.driver as AppiumDriver + } + def cleanupSpec() { + // Quit the driver if it is initialized + driver.quit() + } + def setup() { + // Initialize the Sauce Labs result to false + sauceLabsResult = false + } + def cleanup() { + // Set the job result in + driver.executeScript("sauce:job-result=$sauceLabsResult") + } + + // This function sets the Sauce Labs result to true, indicating that the test passed successfully. + // We use this variable to inform Sauce Labs about the test results, storing whether there were any failures. + // By setting it to true at the end of each test, we ensure that Sauce Labs is updated with the correct test status. + def setTrueResultForSauceLabs() { + (sauceLabsResult = true) != null + } + + // Add this @Tag to include this test in the 'test_mobile_app' group. + // This tag ensures the test runs in the Environment.MOBILE_APP configuration. + @Tag("test_mobile_app") + def "check elements in the first page of the app"() { + given: "Launching the app" + when: "Printing all the elements" + // Verify if the specific element is present + List specificElements = driver.findElements(AppiumBy.name("Cart-tab-item")) + + then: "The specific element should be present" + assert specificElements != null && !specificElements.isEmpty() + assert specificElements[0] != null + + // Click on the element + specificElements[0].click() + // Print evidence for the specific element + SpecHelper.printEvidenceForWebElement(this, 1, specificElements[0], "Cart-tab-item Element Evidence") + + // Set the Sauce Labs result to true, indicating that the test passed successfully + setTrueResultForSauceLabs() + } + +} diff --git a/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileGebHomePageSpec.groovy b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileGebHomePageSpec.groovy new file mode 100644 index 000000000..3e8590f3a --- /dev/null +++ b/e2e-spock-geb/files/src/test/acceptance/groovy/specs/DemoMobileGebHomePageSpec.groovy @@ -0,0 +1,70 @@ +package specs + +import geb.spock.GebReportingSpec +import io.appium.java_client.AppiumDriver +import org.openqa.selenium.WebElement +import spock.lang.Shared +import spock.lang.Stepwise +import spock.lang.Tag +import helpers.SpecHelper + +@Stepwise +class DemoMobileGebHomePageSpec extends GebReportingSpec { + + // Shared driver instance for the AppiumDriver + @Shared + def static driver + // Shared variable to indicate the success of the test for Sauce Labs + @Shared + def static sauceLabsResult = false + + def setupSpec() { + // Initialize the Appium driver + driver = browser.driver as AppiumDriver + } + def cleanupSpec() { + // Quit the driver if it is initialized + driver.quit() + } + def setup() { + // Initialize the Sauce Labs result to false + sauceLabsResult = false + } + def cleanup() { + // Set the job result in + driver.executeScript("sauce:job-result=$sauceLabsResult") + } + + // This function sets the Sauce Labs result to true, indicating that the test passed successfully. + // We use this variable to inform Sauce Labs about the test results, storing whether there were any failures. + // By setting it to true at the end of each test, we ensure that Sauce Labs is updated with the correct test status. + def setTrueResultForSauceLabs() { + (sauceLabsResult = true) != null + } + + // Add this @Tag to include this test in the 'test_mobile_browser' group. + // This tag ensures the test runs in the Environment.MOBILE_BROWSER configuration. + @Tag("test_mobile_browser") + def "verify geb home page and documentation navigation"() { + when: "Navigating to Geb home page" + // Open the Geb home page + driver.get("https://www.gebish.org") + then: "The page title should be 'Geb - Very Groovy Browser Automation'" + // Verify the page title + assert title == "Geb - Very Groovy Browser Automation" + when: "Accessing to the Documentation page" + // Wait for the Documentation button to be displayed and click it + waitFor { $("button.ui.blue.button", text: "Documentation").displayed } + WebElement documentationButton = $("button.ui.blue.button", text: "Documentation").firstElement() + documentationButton.click() + + // Print evidence for the Documentation button + SpecHelper.printEvidenceForWebElement(this, 1, documentationButton, "Documentation Button Evidence") + then: "The page title should be 'The Book Of Geb'" + // Verify the page title + assert title == "The Book Of Geb" + // Set the Sauce Labs result to true, indicating that the test passed successfully + setTrueResultForSauceLabs() + } + +} diff --git a/e2e-spock-geb/files/src/test/acceptance/java/DemoAcceptanceTest.java b/e2e-spock-geb/files/src/test/acceptance/java/DemoAcceptanceTest.java index f2cfd8341..fe0c20363 100644 --- a/e2e-spock-geb/files/src/test/acceptance/java/DemoAcceptanceTest.java +++ b/e2e-spock-geb/files/src/test/acceptance/java/DemoAcceptanceTest.java @@ -1,11 +1,12 @@ - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import spock.lang.Tag; public class DemoAcceptanceTest { @Test void basicTest() { + // Assert that the condition is true Assertions.assertTrue(true); } diff --git a/e2e-spock-geb/files/src/test/installation/groovy/DemoInstallationSpec.groovy b/e2e-spock-geb/files/src/test/installation/groovy/DemoInstallationSpec.groovy index 8280be79d..ab48d5e6a 100644 --- a/e2e-spock-geb/files/src/test/installation/groovy/DemoInstallationSpec.groovy +++ b/e2e-spock-geb/files/src/test/installation/groovy/DemoInstallationSpec.groovy @@ -1,8 +1,9 @@ import geb.spock.GebReportingSpec import spock.lang.Stepwise +import spock.lang.Tag @Stepwise -class DemoInstallation extends GebReportingSpec { +class DemoInstallationSpec extends GebReportingSpec { def "basic test"() { given: "Example test" diff --git a/e2e-spock-geb/files/src/test/installation/java/DemoInstallationTest.java b/e2e-spock-geb/files/src/test/installation/java/DemoInstallationTest.java index d5cd905b7..3056dceff 100644 --- a/e2e-spock-geb/files/src/test/installation/java/DemoInstallationTest.java +++ b/e2e-spock-geb/files/src/test/installation/java/DemoInstallationTest.java @@ -1,11 +1,12 @@ - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import spock.lang.Tag; public class DemoInstallationTest { @Test void basicTest() { + // Assert that the condition is true Assertions.assertTrue(true); } diff --git a/e2e-spock-geb/files/src/test/integration/groovy/DemoIntegrationSpec.groovy b/e2e-spock-geb/files/src/test/integration/groovy/DemoIntegrationSpec.groovy index 432926a67..ebfb28c85 100644 --- a/e2e-spock-geb/files/src/test/integration/groovy/DemoIntegrationSpec.groovy +++ b/e2e-spock-geb/files/src/test/integration/groovy/DemoIntegrationSpec.groovy @@ -1,12 +1,13 @@ import geb.spock.GebReportingSpec import spock.lang.Stepwise +import spock.lang.Tag @Stepwise -class DemoIntegration extends GebReportingSpec { +class DemoIntegrationSpec extends GebReportingSpec { def "basic test"() { - given: "Example test" + given: "An example test scenario" + // This is a placeholder assertion for demonstration purposes true == true } } - diff --git a/e2e-spock-geb/files/src/test/integration/java/DemoIntegrationTest.java b/e2e-spock-geb/files/src/test/integration/java/DemoIntegrationTest.java index 9c86d92a1..27d0604e3 100644 --- a/e2e-spock-geb/files/src/test/integration/java/DemoIntegrationTest.java +++ b/e2e-spock-geb/files/src/test/integration/java/DemoIntegrationTest.java @@ -1,11 +1,12 @@ - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import spock.lang.Tag; public class DemoIntegrationTest { @Test void basicTest() { + // Assert that the condition is true Assertions.assertTrue(true); } diff --git a/e2e-spock-geb/files/src/test/resources/GebConfig.groovy b/e2e-spock-geb/files/src/test/resources/GebConfig.groovy index a6a86bc7f..2f775af9c 100644 --- a/e2e-spock-geb/files/src/test/resources/GebConfig.groovy +++ b/e2e-spock-geb/files/src/test/resources/GebConfig.groovy @@ -1,35 +1,93 @@ import com.gargoylesoftware.htmlunit.BrowserVersion import com.gargoylesoftware.htmlunit.WebClient +import io.appium.java_client.android.AndroidDriver +import io.appium.java_client.ios.IOSDriver +import org.openqa.selenium.MutableCapabilities import org.openqa.selenium.htmlunit.HtmlUnitDriver import org.openqa.selenium.Proxy +import helpers.* -// Load application.properties +// Load application properties from application.properties file def properties = new SpecHelper().getApplicationProperties() -// Selenium driver (True in constructor to use JavaScript) -driver = { - HtmlUnitDriver driver = new HtmlUnitDriver(BrowserVersion.BEST_SUPPORTED, true) { - @Override - protected WebClient newWebClient(BrowserVersion version) { - WebClient webClient = super.newWebClient(version); - // don't throw on script errors - webClient.getOptions().setThrowExceptionOnScriptError(false); - return webClient; - } - }; - - def env = System.getenv() - if(env.HTTP_PROXY) { - Proxy proxy = new Proxy(); - URL url = new URL(env.HTTP_PROXY); - proxy.setHttpProxy("${url.getHost()}:${url.getPort()}"); - proxy.setNoProxy(env.NO_PROXY) - driver.setProxySettings(proxy); +// Get SauceLabs environment variables for configuring iOS device +def sauceLabsUsername = System.getenv('SAUCE_LABS_USERNAME') +def sauceLabsAccessKey = System.getenv('SAUCE_LABS_ACCESS_KEY') + +environments { + + // Configuration for desktop environment using HtmlUnitDriver + "${Environments.DESKTOP}" { + driver = { + HtmlUnitDriver driver = new HtmlUnitDriver(BrowserVersion.BEST_SUPPORTED, true) { + @Override + protected WebClient newWebClient(BrowserVersion version) { + WebClient webClient = super.newWebClient(version) + webClient.getOptions().setThrowExceptionOnScriptError(false) + webClient.getOptions().setCssEnabled(false) + return webClient + } + } + def env = System.getenv() + if(env.HTTP_PROXY) { + Proxy proxy = new Proxy(); + URL url = new URL(env.HTTP_PROXY); + proxy.setHttpProxy("${url.getHost()}:${url.getPort()}"); + proxy.setNoProxy(env.NO_PROXY) + driver.setProxySettings(proxy); + } + return driver + } } - return driver + // Configuration for mobile browser environment using AndroidDriver + "${Environments.MOBILE_BROWSER}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + caps.setCapability("platformName", "Android") + caps.setCapability("browserName", "Chrome") + caps.setCapability("appium:deviceName", "Google Pixel 7a GoogleAPI Emulator") + caps.setCapability("appium:platformVersion", "13.0") + caps.setCapability("appium:automationName", "UiAutomator2") + MutableCapabilities sauceOptions = new MutableCapabilities() + sauceOptions.setCapability("appiumVersion", "2.11.0") + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) + sauceOptions.setCapability("build", "") + sauceOptions.setCapability("name", "") + sauceOptions.setCapability("deviceOrientation", "PORTRAIT") + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + AndroidDriver driver = new AndroidDriver(url, caps) + return driver + } + } + + // Configuration for mobile app environment using IOSDriver + "${Environments.MOBILE_APP}" { + driver = { + MutableCapabilities caps = new MutableCapabilities() + caps.setCapability("platformName", "iOS") + caps.setCapability("appium:app", "storage:filename=SauceLabs-Demo-App.Simulator.XCUITest.zip") // The filename of the mobile app + caps.setCapability("appium:deviceName", "iPhone Simulator") + caps.setCapability("appium:platformVersion", "17.0") + caps.setCapability("appium:automationName", "XCUITest") + MutableCapabilities sauceOptions = new MutableCapabilities() + sauceOptions.setCapability("appiumVersion", "2.1.3") + sauceOptions.setCapability("username", sauceLabsUsername) + sauceOptions.setCapability("accessKey", sauceLabsAccessKey) + sauceOptions.setCapability("build", "") + sauceOptions.setCapability("name", "") + sauceOptions.setCapability("deviceOrientation", "PORTRAIT") + caps.setCapability("sauce:options", sauceOptions) + URL url = new URL("https://ondemand.eu-central-1.saucelabs.com:443/wd/hub") + IOSDriver driver = new IOSDriver(url, caps) + return driver + } + } } +// Configure waiting settings waiting { timeout = 25 retryInterval = 0.5 @@ -45,9 +103,8 @@ waiting { } } -// Base URL of the application to test +// Set the base URL of the application to test baseUrl = properties."config.application.url" -// Reports dir +// Set the directory for storing reports reportsDir = new File(properties."config.reports.dir") - diff --git a/e2e-spock-geb/files/src/test/resources/SpecHelper.groovy b/e2e-spock-geb/files/src/test/resources/SpecHelper.groovy deleted file mode 100644 index 7f7287a69..000000000 --- a/e2e-spock-geb/files/src/test/resources/SpecHelper.groovy +++ /dev/null @@ -1,89 +0,0 @@ -import geb.Page -import geb.navigator.Navigator -import geb.spock.GebReportingSpec - -import java.util.regex.Pattern - -class SpecHelper { - - public Properties getApplicationProperties() { - def env = System.getenv() - - def properties = new Properties() - this.getClass().getResource('/application.properties').withInputStream { - properties.load(it) - } - - properties.each { key, value -> - def matcher = value =~ /\$\{(.*?)\}/ - if (matcher.find()) { - matcher.each { match -> - def nameToReplace = match[0] - def valueToReplace = env[match[1]] - - value = value.replaceAll(Pattern.quote(nameToReplace), valueToReplace) - properties[key] = value - } - } - } - - return properties - } - - public static void printEvidenceForPageElement(GebReportingSpec spec, int testStepNumber, Navigator fragment, String description = '', int desiredLevel = -1) { - printEvidenceForPageElements(spec, testStepNumber, [ [ 'fragment' : fragment, 'description' : description] ], desiredLevel) - } - - public static void printEvidenceForPageElements(GebReportingSpec spec, int testStepNumber, List fragmentsAndDiscriptions, int desiredLevel = -1) { - println '=====================================' - println "Test Case: ${spec.specificationContext.currentIteration.name}" - println "Test Step: ${testStepNumber}" - println "Page URL: ${spec.getBrowser().getPage().getDriver().getCurrentUrl()}" - - if (!fragmentsAndDiscriptions || fragmentsAndDiscriptions.isEmpty()) { - throw new IllegalArgumentException("Error: evidence fragment is empty!") - } - - println "----- Test Evidence STARTS Here -----" - fragmentsAndDiscriptions.each { fragmentAndDescription -> - println "Description: ${fragmentAndDescription.description}" - printEvidenceRecursive(fragmentAndDescription.fragment, 0, desiredLevel) - } - println "----- Test Evidence ENDS Here -----" - } - - private static printEvidenceRecursive(Navigator fragment, int level = 0, int desiredLevel = -1) { - if (!fragment) { - return - } - - if (level == desiredLevel) { - return - } - - // create fragmentIndentation based on level - String fragmentIndentation = '' - for (int i = 0; i < level; i++) { - fragmentIndentation += ' ' - } - - // create the prepend for the fragment, current level, plus strings - String fragmentPrepend = "(${level})${fragmentIndentation}" - - Navigator children = fragment.children(); - // no children of current fragment - print fragment and content - // (value for input types, text or for images or links, nothing) - if (children.size() == 0) { - def value = fragment.value() ?: fragment.text() - println "${fragmentPrepend}${fragment} ${value}" - } else { - // print the current fragment without any content - println "${fragmentPrepend}${fragment}" - - // for each child print the evidence recursively - children.each { child -> - printEvidenceRecursive(child, (level + 1), desiredLevel) - } - } - } -} diff --git a/e2e-spock-geb/files/src/test/resources/application.properties b/e2e-spock-geb/files/src/test/resources/application.properties index a7bed91de..1ef88e30a 100644 --- a/e2e-spock-geb/files/src/test/resources/application.properties +++ b/e2e-spock-geb/files/src/test/resources/application.properties @@ -1,4 +1,4 @@ -# Base url with the application to test - w3c schools +# Base url with the application to test config.application.url=https://github.com # Base url with the application to test - OpenShift app using env vars from Jenkinsfile diff --git a/e2e-spock-geb/files/src/test/resources/helpers/Environments.groovy b/e2e-spock-geb/files/src/test/resources/helpers/Environments.groovy new file mode 100644 index 000000000..eb547f961 --- /dev/null +++ b/e2e-spock-geb/files/src/test/resources/helpers/Environments.groovy @@ -0,0 +1,9 @@ +package helpers + +class Environments { + // Define environment constants using system properties + // Check the build.gradle file for the defined properties + static final String DESKTOP = System.getProperty('environments.desktop') + static final String MOBILE_BROWSER = System.getProperty('environments.mobile_browser') + static final String MOBILE_APP = System.getProperty('environments.mobile_app') +} diff --git a/e2e-spock-geb/files/src/test/resources/helpers/SpecHelper.groovy b/e2e-spock-geb/files/src/test/resources/helpers/SpecHelper.groovy new file mode 100644 index 000000000..24fcb004f --- /dev/null +++ b/e2e-spock-geb/files/src/test/resources/helpers/SpecHelper.groovy @@ -0,0 +1,175 @@ +package helpers + +import geb.navigator.Navigator +import geb.spock.GebReportingSpec +import io.appium.java_client.AppiumDriver +import org.openqa.selenium.By +import org.openqa.selenium.StaleElementReferenceException +import org.openqa.selenium.WebDriver +import org.openqa.selenium.WebElement + +import java.util.regex.Pattern + +class SpecHelper { + + // Load application properties and replace placeholders with environment variables + public Properties getApplicationProperties() { + def env = System.getenv() + + def properties = new Properties() + this.getClass().getResource('/application.properties').withInputStream { + properties.load(it) + } + + properties.each { key, value -> + def matcher = value =~ /\$\{(.*?)\}/ + if (matcher.find()) { + matcher.each { match -> + def nameToReplace = match[0] + def valueToReplace = env[match[1]] + + value = value.replaceAll(Pattern.quote(nameToReplace), valueToReplace) + properties[key] = value + } + } + } + + return properties + } + + // Print evidence for a single page element + public static void printEvidenceForPageElement(GebReportingSpec spec, int testStepNumber, Navigator fragment, String description = '', int desiredLevel = -1) { + printEvidenceForPageElements(spec, testStepNumber, [ [ 'fragment' : fragment, 'description' : description] ], desiredLevel) + } + + // Print evidence for multiple page elements + public static void printEvidenceForPageElements(GebReportingSpec spec, int testStepNumber, List fragmentsAndDescriptions, int desiredLevel = -1) { + println '=====================================' + println "Test Case: ${spec.specificationContext.currentIteration.name}" + println "Test Step: ${testStepNumber}" + println "Page URL: ${spec.getBrowser().getPage().getDriver().getCurrentUrl()}" + + if (!fragmentsAndDescriptions || fragmentsAndDescriptions.isEmpty()) { + throw new IllegalArgumentException("Error: evidence fragment is empty!") + } + + println "----- Test Evidence STARTS Here -----" + fragmentsAndDescriptions.each { fragmentAndDescription -> + println "Description: ${fragmentAndDescription.description}" + printEvidenceRecursive(fragmentAndDescription.fragment, 0, desiredLevel) + } + println "----- Test Evidence ENDS Here -----" + } + + // Recursively print evidence for a page element and its children + private static void printEvidenceRecursive(Navigator fragment, int level = 0, int desiredLevel = -1) { + if (!fragment) { + return + } + + if (level == desiredLevel) { + return + } + + // Create indentation based on the level + String fragmentIndentation = '' + for (int i = 0; i < level; i++) { + fragmentIndentation += ' ' + } + + // Prepend the fragment with the current level and indentation + String fragmentPrepend = "(${level})${fragmentIndentation}" + + Navigator children = fragment.children() + // If no children, print the fragment and its content + if (children.size() == 0) { + def value = fragment.value() ?: fragment.text() + println "${fragmentPrepend}${fragment} ${value}" + } else { + // Print the current fragment without content + println "${fragmentPrepend}${fragment}" + + // Recursively print each child + children.each { child -> + printEvidenceRecursive(child, (level + 1), desiredLevel) + } + } + } + + // Print evidence for a single web element + public static void printEvidenceForWebElement(GebReportingSpec spec, int testStepNumber, WebElement element, String description = '', int desiredLevel = -1) { + printEvidenceForWebElements(spec, testStepNumber, [ ['element': element, 'description': description] ], desiredLevel) + } + + // Print evidence for multiple web elements + public static void printEvidenceForWebElements(GebReportingSpec spec, int testStepNumber, List elementsAndDescriptions, int desiredLevel = -1) { + println '=====================================' + println "Test Case: ${spec.specificationContext.currentIteration.name}" + println "Test Step: ${testStepNumber}" + + if (!elementsAndDescriptions || elementsAndDescriptions.isEmpty()) { + throw new IllegalArgumentException("Error: evidence element is empty!") + } + + WebDriver driver = spec.getBrowser().getPage().getDriver() + if (driver instanceof AppiumDriver) { + println "Page Source: ${driver.getPageSource()}" + } else { + println "Page URL: ${driver.getCurrentUrl()}" + } + + println "----- Test Evidence STARTS Here -----" + elementsAndDescriptions.each { elementAndDescriptions -> + println "Description: ${elementAndDescriptions.description}" + printEvidenceRecursive(elementAndDescriptions.element, 0, desiredLevel) + } + println "----- Test Evidence ENDS Here -----" + } + + // Recursively print evidence for a web element and its children + private static void printEvidenceRecursive(WebElement element, int level = 0, int desiredLevel = -1) { + if (element == null) { + return + } + + if (level == desiredLevel) { + return + } + + // Create indentation based on the level + String fragmentIndentation = '' + for (int i = 0; i < level; i++) { + fragmentIndentation += ' ' + } + + // Prepend the element with the current level and indentation + String fragmentPrepend = "(${level})${fragmentIndentation}" + + List children + try { + children = element.findElements(By.xpath("./*")) + } catch (StaleElementReferenceException e) { + println "${fragmentPrepend}${element} [Stale Element]" + return + } + + // If no children, print the element and its content + if (children.isEmpty()) { + try { + def value = element.getAttribute("value") ?: element.getText() + println "${fragmentPrepend}${element} ${value}" + } catch (StaleElementReferenceException e) { + println "${fragmentPrepend}${element} [Stale Element]" + } + } else { + // Print the current element without content + println "${fragmentPrepend}${element}" + + // Recursively print each child + children.each { child -> + printEvidenceRecursive(child, (level + 1), desiredLevel) + } + } + } + +}