diff --git a/README.md b/README.md index c422780..bf96052 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,16 @@ Geb Functional Testing for the Grails® framework. +This plugin provides the Geb dependencies, a `create-functional-test` command for generating Geb tests in a Grails app +and also adds a ContainerGebSpec that when inherited from automatically runs the browser in a container. + This plugin provides the Geb dependencies and a `create-functional-test` command for generating Geb tests in a Grails app. +It also provides a `ContainerGebSpec` class, which can be used in place of `GebSpec`, that automatically +runs the browser in a container using [Testcontainers](https://java.testcontainers.org/). This requires a +[compatible container runtime](https://java.testcontainers.org/supported_docker_environment/) such as +[Docker](https://www.docker.com/) to be installed. + For further reference please see the [Geb documentation](https://www.gebish.org). ## Examples diff --git a/build.gradle b/build.gradle index 0838a4d..08857db 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,30 @@ buildscript { repositories { - maven { url "https://repo.grails.org/grails/core" } + maven { url = 'https://repo.grails.org/grails/core' } } dependencies { classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" } } -version projectVersion -group "org.grails.plugins" +version = projectVersion +group = 'org.grails.plugins' apply plugin: 'java-library' -apply plugin: 'idea' +apply plugin: 'java-test-fixtures' apply plugin: 'org.grails.grails-plugin' apply plugin: 'org.grails.internal.grails-plugin-publish' +apply plugin: 'maven-publish' java { toolchain { languageVersion = JavaLanguageVersion.of(17) + vendor = JvmVendorSpec.BELLSOFT } } repositories { - maven { url "https://repo.grails.org/grails/core" } + maven { url = 'https://repo.grails.org/grails/core' } } configurations { @@ -30,20 +32,24 @@ configurations { } dependencies { - compileOnly "org.grails:grails-core:$grailsVersion" - // used transitively by the generated tests - api "org.gebish:geb-spock:$gebSpock" - api "org.grails:grails-testing-support:$testingSupportVersion" - api "org.grails:grails-datastore-gorm:$datastoreVersion" + implementation(platform("org.grails:grails-bom:$grailsVersion")) - documentation "org.apache.groovy:groovy:$groovyVersion" - documentation "org.apache.groovy:groovy-ant:$groovyVersion" - documentation "org.apache.groovy:groovy-templates:$groovyVersion" - documentation "com.github.javaparser:javaparser-core:$javaParserCoreVersion" -} + compileOnly 'org.grails:grails-core' // Provided as this is a Grails plugin + + testFixturesCompileOnly 'jakarta.servlet:jakarta.servlet-api' + testFixturesApi 'org.gebish:geb-spock' + testFixturesApi 'org.grails:grails-testing-support' + testFixturesApi 'org.grails:grails-datastore-gorm' + testFixturesApi "org.testcontainers:selenium:$testcontainersVersion" + testFixturesApi "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion" + testFixturesApi "org.seleniumhq.selenium:selenium-remote-driver:$seleniumVersion" -findMainClass.enabled = false + documentation 'org.apache.groovy:groovy' + documentation 'org.apache.groovy:groovy-ant' + documentation 'org.apache.groovy:groovy-templates' + documentation 'com.github.javaparser:javaparser-core' +} grailsPublish { userOrg = 'grails' @@ -51,38 +57,34 @@ grailsPublish { license { name = 'Apache-2.0' } - title = "Grails Geb Plugin" - desc = "Provides Integration with Geb for Functional Testing" - developers = [graemerocher: "Graeme Rocher", puneetbehl: "Puneet Behl"] + title = 'Grails Geb Plugin' + desc = 'Provides Integration with Geb for Functional Testing' + developers = [ + graemerocher: 'Graeme Rocher', + puneetbehl: 'Puneet Behl', + sbglasius: 'Søren Berg Glasius', + matrei: 'Mattias Reichel' + ] } -tasks.withType(Groovydoc) { - destinationDir = new File(buildDir, 'docs/api') +tasks.withType(Groovydoc).configureEach { + destinationDir = layout.buildDirectory.dir('docs/api').get().asFile docTitle = "Grails Geb Plugin ${version}" classpath = configurations.documentation } -tasks.withType(GroovyCompile) { - configure(groovyOptions) { - forkOptions.jvmArgs = ['-Xmx1024m'] - } -} - -tasks.withType(Test) { +tasks.withType(Test).configureEach { useJUnitPlatform() -} - -test { testLogging { - events "passed", "skipped", "failed" - + events 'passed', 'skipped', 'failed' showExceptions true - exceptionFormat "full" + exceptionFormat 'full' showCauses true showStackTraces true } } -bootJar.enabled = false -bootRun.enabled = false -bootTestRun.enabled = false \ No newline at end of file +tasks.named('bootJar') { enabled = false } +tasks.named('bootRun') { enabled = false } +tasks.named('bootTestRun') { enabled = false } +tasks.named('findMainClass') { enabled = false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 8b2ebed..b82b693 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,12 @@ projectVersion=5.0.0-SNAPSHOT -title=Grails Geb Plugin -developers=Puneet Behl - -datastoreVersion=9.0.0-SNAPSHOT -gebSpock=7.0 grailsVersion=7.0.0-SNAPSHOT grailsGradlePluginVersion=7.0.0-SNAPSHOT -groovyVersion=4.0.23 -javaParserCoreVersion=3.26.2 -testingSupportVersion=4.0.0-SNAPSHOT +seleniumVersion=4.25.0 +testcontainersVersion=1.20.2 + +# This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs +# https://github.com/grails/grails-gradle-plugin/issues/222 +slf4jPreventExclusion=true org.gradle.parallel=true org.gradle.caching=true diff --git a/settings.gradle b/settings.gradle index e3273ca..5e7b294 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,30 +1,27 @@ plugins { - id "com.gradle.enterprise" version "3.18.1" + id 'com.gradle.develocity' version '3.18.1' id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } -rootProject.name="geb" +def isCI = System.getenv('CI') != null +def isLocal = !isCI +def isAuthenticated = System.getenv('DEVELOCITY_ACCESS_KEY') != null -gradleEnterprise { +develocity { server = 'https://ge.grails.org' buildScan { - publishAlways() - publishIfAuthenticated() - uploadInBackground = System.getenv("CI") == null - capture { - taskInputFiles = true - } + publishing.onlyIf { isAuthenticated } + uploadInBackground = isLocal } } buildCache { - local { enabled = System.getenv('CI') != 'true' } - remote(HttpBuildCache) { - push = System.getenv('CI') == 'true' + local { enabled = isLocal } + remote(develocity.buildCache) { + push = isCI && isAuthenticated enabled = true - url = 'https://ge.grails.org/cache/' - credentials { - username = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER') - password = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY') - } - }} + } +} + +rootProject.name = 'geb' diff --git a/src/main/groovy/geb/GebGrailsPlugin.groovy b/src/main/groovy/grails/plugin/geb/GebGrailsPlugin.groovy similarity index 96% rename from src/main/groovy/geb/GebGrailsPlugin.groovy rename to src/main/groovy/grails/plugin/geb/GebGrailsPlugin.groovy index bde67dd..8884a3a 100644 --- a/src/main/groovy/geb/GebGrailsPlugin.groovy +++ b/src/main/groovy/grails/plugin/geb/GebGrailsPlugin.groovy @@ -1,4 +1,4 @@ -package geb +package grails.plugin.geb import grails.plugins.Plugin import grails.plugins.metadata.PluginSource diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy new file mode 100644 index 0000000..4da8ba5 --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy @@ -0,0 +1,108 @@ +/* + * Copyright 2024 original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.plugin.geb + +import geb.spock.GebSpec +import groovy.transform.PackageScope +import org.openqa.selenium.chrome.ChromeOptions +import org.openqa.selenium.remote.RemoteWebDriver +import org.testcontainers.Testcontainers +import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.containers.PortForwardingContainer +import spock.lang.Shared + +import java.time.Duration + +/** + * A {@link geb.spock.GebSpec GebSpec} that leverages Testcontainers to run the browser inside a container. + * + *

Prerequisites: + *

+ * + * @author Søren Berg Glasius + * @author Mattias Reichel + * @since 5.0.0 + */ +class ContainerGebSpec extends GebSpec { + + private static final String DEFAULT_PROTOCOL = 'http' + private static final String DEFAULT_HOSTNAME = 'host.testcontainers.internal' + + @Shared + BrowserWebDriverContainer webDriverContainer + + @PackageScope + void initialize() { + if (!webDriverContainer) { + if (!hasProperty('serverPort')) { + throw new IllegalStateException('Test class must be annotated with @Integration for serverPort to be injected') + } + webDriverContainer = new BrowserWebDriverContainer() + webDriverContainer.tap { + addExposedPort(serverPort) + withAccessToHost(true) + start() + } + Testcontainers.exposeHostPorts(serverPort) + if (hostName != DEFAULT_HOSTNAME) { + webDriverContainer.execInContainer('/bin/sh', '-c', "echo '$hostIp\t$hostName' | sudo tee -a /etc/hosts") + } + browser.driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions()) + browser.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)) + } + } + + void setup() { + initialize() + baseUrl = "$protocol://$hostName:$serverPort" + } + + def cleanupSpec() { + webDriverContainer.stop() + } + + /** + * Returns the protocol that the browser will use to access the server under test. + *

Defaults to {@code http}. + * + * @return the protocol for accessing the server under test + */ + String getProtocol() { + return DEFAULT_PROTOCOL + } + + /** + * Returns the hostname that the browser will use to access the server under test. + *

Defaults to {@code host.testcontainers.internal}. + * + * @return the hostname for accessing the server under test + */ + String getHostName() { + return DEFAULT_HOSTNAME + } + + private static String getHostIp() { + PortForwardingContainer.INSTANCE.network.get().ipAddress + } +} \ No newline at end of file