Skip to content
Merged

Geb 8 #15067

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e938b04
fix(deps): `geb` v. `7.0` -> `8.0.0`
matrei Sep 10, 2025
7678b16
Merge branch '7.0.x' into geb-8
matrei Sep 13, 2025
0a79f58
fix: adapt to changes in Geb 8
matrei Sep 15, 2025
93d6756
refactor(geb): simplifications
matrei Sep 15, 2025
22cbeaf
docs(geb): improve comments and javadoc
matrei Sep 15, 2025
07e0a36
chore(geb): whitespace
matrei Sep 15, 2025
46ca9e6
fix(geb): reset `webdriver.remote.server` after `RemoteWebDriver` init
matrei Sep 16, 2025
24286b4
docs(geb): minor comment improvement
matrei Sep 16, 2025
7f5185c
fix(geb): prevent cross-thread leakage of `webdriver.remote.server`
matrei Sep 16, 2025
fcc4d82
fix(geb): allow testing with different browsers
matrei Sep 16, 2025
3f78a05
chore(geb): formatting
matrei Sep 16, 2025
336e1c9
chore(geb): formatting
matrei Sep 17, 2025
5599e70
refactor(geb): simplify system property override
matrei Sep 17, 2025
14ab0a3
docs(geb): improve javadoc and code comments
matrei Sep 17, 2025
510e820
fix(geb): resolve trait method conflicts via explicit override
matrei Sep 17, 2025
4a4698c
refactor(geb): simplify
matrei Sep 17, 2025
2f55451
chore(geb): formatting
matrei Sep 17, 2025
25f8c44
refactor(geb): simplify
matrei Sep 17, 2025
a11d692
test(geb): update test label
matrei Sep 17, 2025
66b0ffb
docs(geb): add `GebConfig` instructions to README
matrei Sep 17, 2025
abda818
fix(geb): make inner classes `static`
matrei Sep 18, 2025
46594c7
feedback(geb): rename property to `containerBrowser`
matrei Sep 18, 2025
264127f
chore(geb): formatting
matrei Sep 18, 2025
77e64e6
fix(geb): add message for missing `containerBrowser`
matrei Sep 18, 2025
19ebcd0
test(geb): make tests more resilient in slow runners
matrei Sep 18, 2025
b814620
fix(feedback): skip `containerBrowser` validation
matrei Sep 19, 2025
c84d971
refactor(geb): extract methods
matrei Sep 19, 2025
cc17f14
fix(geb): move `atCheckWaiting` to CI env variable
matrei Sep 19, 2025
79b8708
fix(geb): feedback - remove container fallback
matrei Sep 22, 2025
e56e484
fix(geb): system property setting of timeout values
matrei Sep 22, 2025
de3f2f4
ci: update Geb `atCheckWaiting` system property
matrei Sep 22, 2025
16957e1
chore(geb): formatting
matrei Sep 22, 2025
4a407cb
fix(geb): use correct `atCheckWaiting` system property
matrei Sep 23, 2025
933057b
fix(geb): feedback - use `BigDecimal` in `GrailsGebSettings`
matrei Sep 24, 2025
93a3259
ci: feedback - set `atCheckWaiting` in build file
matrei Sep 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ jobs:
--continue
--rerun-tasks
--stacktrace
-PgebAtCheckWaiting
-PonlyFunctionalTests
-PskipCodeStyle
-PskipHibernate5Tests
Expand Down
1 change: 1 addition & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ These can be set on the command line like so:

`./gradlew check -PskipCodeStyle`

* `gebAtCheckWaiting` - enables Geb atCheckWaiting
* `onlyCoreTests` - runs tests that do not include mongo, hibernate, or functional
* `onlyFunctionalTests` - runs only grails-test-examples/* tests
* `onlyHibernate5Tests` - runs only a hibernate5 related test
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ allprojects {
url = 'https://repository.apache.org/content/groups/staging'
content {
includeModuleByRegex('org[.]apache[.]grails[.]gradle', 'grails-publish')
includeModuleByRegex('org[.]apache[.]groovy[.]geb', 'geb-.*')
}
mavenContent {
releasesOnly()
Expand Down
4 changes: 2 additions & 2 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ ext {
'bootstrap-icons.version' : '1.13.1',
'bootstrap.version' : '5.3.7',
'commons-codec.version' : '1.18.0',
'geb-spock.version' : '7.0',
'geb-spock.version' : '8.0.0',
'groovy.version' : '4.0.28',
'h2.version' : '2.3.232',
'jackson.version' : '2.19.1',
Expand Down Expand Up @@ -106,7 +106,7 @@ ext {
'bootstrap' : "org.webjars.npm:bootstrap:${bomDependencyVersions['bootstrap.version']}",
'bootstrap-icons' : "org.webjars.npm:bootstrap-icons:${bomDependencyVersions['bootstrap-icons.version']}",
'commons-codec' : "commons-codec:commons-codec:${bomDependencyVersions['commons-codec.version']}",
'geb-spock' : "org.gebish:geb-spock:${bomDependencyVersions['geb-spock.version']}",
'geb-spock' : "org.apache.groovy.geb:geb-spock:${bomDependencyVersions['geb-spock.version']}",
'h2' : "com.h2database:h2:${bomDependencyVersions['h2.version']}",
'jquery' : "org.webjars.npm:jquery:${bomDependencyVersions['jquery.version']}",
'liquibase-hibernate5' : "org.liquibase:liquibase:${bomDependencyVersions['liquibase-hibernate5.version']}",
Expand Down
5 changes: 5 additions & 0 deletions gradle/functional-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ tasks.withType(Test).configureEach { Test task ->
if (System.getProperty('debug.tests')) {
task.jvmArgs += debugArguments
}

// Make Geb tests more resilient in slow CI environments
if (project.hasProperty('gebAtCheckWaiting')) {
systemProperty('grails.geb.atCheckWaiting.enabled', 'true')
}
}

tasks.named('groovydoc').configure {
Expand Down
2 changes: 1 addition & 1 deletion grails-doc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ dependencies {
implementation 'org.testcontainers:testcontainers'
implementation 'org.springframework:spring-core'
implementation 'org.springframework.boot:spring-boot'
implementation 'org.gebish:geb-spock'
implementation 'org.apache.groovy.geb:geb-spock'
}

// this task needs to be here instead of the root since bom resolution only occurs when the java / groovy plugins are applied
Expand Down
40 changes: 39 additions & 1 deletion grails-geb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,34 @@ the `container` from within your `ContainerGebSpec` to, for example, call `.copy
An Example of this can be seen in [ContainerSupport#createFileInputSource utility method](./src/testFixtures/groovy/grails/plugin/geb/support/ContainerSupport.groovy).

#### Timeouts

The following system properties exist to configure timeouts:

* `grails.geb.atCheckWaiting.enabled`
* purpose: if `at` checks should wait for the page to be in the expected state (uses configured waiting timeout values)
* type: boolean
* defaults to `false`
* `grails.geb.timeouts.retryInterval`
* purpose: how often to retry waiting operations
* type: Number
* defaults to `0.1` seconds
* `grails.geb.timeouts.waiting`
* purpose: amount of time to wait for waiting operations
* type: Number
* defaults to `5.0` seconds
* `grails.geb.timeouts.implicitlyWait`
* purpose: amount of time the driver should wait when searching for an element if it is not immediately present.
* type: int
* defaults to `0` seconds, which means that if an element is not found, it will immediately return an error.
* Warning: Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times.
Consult the [Geb](https://groovy.apache.org/geb/manual/current/#implicit-assertions-waiting)
and/or [Selenium](https://www.selenium.dev/documentation/webdriver/waits/) documentation for details.
* `grails.geb.timeouts.pageLoad`
* purpose: amount of time to wait for a page load to complete before throwing an error.
* type: int
* defaults to `300` seconds
* `grails.geb.timeouts.script`
* purpose: amount of time to wait for an asynchronous script to finish execution before throwing an error.
* type: int
* defaults to `30` seconds

#### Observability and Tracing
Expand All @@ -161,6 +177,28 @@ To enable tracing, set the following system property:

This allows you to opt in to tracing when an OpenTelemetry collector is available.

#### GebConfig.groovy and using non-default browser settings
Provide a `GebConfig.groovy` on the test runtime classpath (commonly `src/integration-test/resources`, but any location on the test classpath works) to customize the browser.

To make this work, ensure:
1. The `driver` property in your `GebConfig` is a `Closure` that returns a `RemoteWebDriver` instance.
2. You set a custom `containerBrowser` property so that `ContainerGebSpec` can start a matching container (e.g. "chrome", "edge", "firefox"). For a list of supported browsers, see the [Testcontainers documentation](https://java.testcontainers.org/modules/webdriver_containers/#other-browsers).
3. Your `build.gradle` includes the driver dependency for the chosen browser.

Example `GebConfig.groovy`:
```groovy
driver = {
new RemoteWebDriver(new FireFoxOptions())
}
containerBrowser = 'firefox'
```
Example `build.gradle`:
```groovy
dependencies {
integrationTestImplementation 'org.seleniumhq.selenium:selenium-firefox-driver'
}
```

### GebSpec

If you choose to extend `GebSpec`, you will need to have a [Selenium WebDriver](https://www.selenium.dev/documentation/webdriver/browsers/) installed that matches a browser you have installed on your system.
Expand Down
4 changes: 2 additions & 2 deletions grails-geb/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ dependencies {
testFixturesCompileOnly 'jakarta.servlet:jakarta.servlet-api'
testFixturesCompileOnly 'org.slf4j:slf4j-simple' // Remove compilation warning about missing slf4j impl

testFixturesApi 'org.gebish:geb-spock'
testFixturesApi 'org.apache.groovy.geb:geb-spock'
testFixturesApi project(':grails-testing-support-core')
testFixturesApi project(':grails-datamapping-core')
testFixturesApi "org.testcontainers:selenium"
Expand All @@ -59,7 +59,7 @@ dependencies {
testFixturesImplementation "org.seleniumhq.selenium:selenium-support"

// Added to be able to resolve the geb version from the BOM in the resolveVersions task
compileOnly 'org.gebish:geb-spock'
compileOnly 'org.apache.groovy.geb:geb-spock'
}

apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import org.openqa.selenium.remote.FileDetector
* An extension of {@link org.openqa.selenium.remote.FileDetector}
* that will get passed additional parameters from the webdriver container holder.
* <p>
* Implementations must provide a zero-argument constructor to ensure compatibility with {@link java.util.ServiceLoader}.
* Implementations must provide a zero-argument constructor to ensure compatibility
* with {@link java.util.ServiceLoader}.
*
* @see GebRecordingTestListener
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import java.lang.annotation.Target
import org.testcontainers.containers.GenericContainer

/**
* Can be used to configure the protocol and hostname that the container's browser will use
* Can be used to configure the protocol and hostname that the container's browser will use.
*
* @author James Daugherty
* @since 4.1
Expand All @@ -53,7 +53,8 @@ import org.testcontainers.containers.GenericContainer
String hostName() default DEFAULT_HOSTNAME_FROM_CONTAINER

/**
* Whether reporting should be enabled for this test. Add a `GebConfig.groovy` to customize the reporter configuration.
* Whether reporting should be enabled for this test.
* Add a `GebConfig.groovy` to customize the reporter configuration.
*/
boolean reporting() default false

Expand All @@ -70,7 +71,7 @@ import org.testcontainers.containers.GenericContainer
}

/**
* Inheritable version of {@link ContainerGebConfiguration}
* Inheritable version of {@link ContainerGebConfiguration}.
*
* @since 4.2
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package grails.plugin.geb

import groovy.transform.CompileStatic

import geb.Page
import geb.test.GebTestManager
import spock.lang.Shared
import spock.lang.Specification
Expand All @@ -32,12 +33,14 @@ import grails.plugin.geb.support.delegate.DriverDelegate
import grails.plugin.geb.support.delegate.PageDelegate

/**
* A {@link geb.spock.GebSpec GebSpec} that leverages Testcontainers to run the browser inside a container.
* A {@link geb.spock.GebSpec GebSpec} that leverages Testcontainers
* to run the browser inside a container.
*
* <p>Prerequisites:
* <ul>
* <li>
* The test class must be annotated with {@link grails.testing.mixin.integration.Integration @Integration}.
* The test class must be annotated with
* {@link grails.testing.mixin.integration.Integration @Integration}.
* </li>
* <li>
* A <a href="https://java.testcontainers.org/supported_docker_environment/">compatible container runtime</a>
Expand All @@ -61,4 +64,10 @@ abstract class ContainerGebSpec extends Specification implements ContainerSuppor
static void setTestManager(GebTestManager testManager) {
this.testManager = testManager
}

@Override
Page getPage() {
// Be explicit which trait to use (PageDelegate vs BrowserDelegate)
PageDelegate.super.page
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import org.spockframework.runtime.model.IterationInfo
import org.testcontainers.lifecycle.TestDescription

/**
* Implements {@link org.testcontainers.lifecycle.TestDescription} to customize recording names.
* Implements {@link org.testcontainers.lifecycle.TestDescription}
* to customize recording names.
*
* @author James Daugherty
* @since 4.1
Expand All @@ -43,7 +44,7 @@ class ContainerGebTestDescription implements TestDescription {
testInfo.displayName != testInfo.feature.displayName ? testInfo.iterationIndex : null
].findAll(/* Remove nulls */).join(' ')

String safeName = testId.replaceAll('\\W+', '_')
def safeName = testId.replaceAll('\\W+', '_')
filesystemFriendlyName = safeName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import org.spockframework.runtime.extension.IMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation

/**
* This class is a direct clone of {@link geb.spock.OnFailureReporter OnFailureReporter}, except it works for the
* {@link grails.plugin.geb.ContainerGebSpec ContainerGebSpec}.
* Adapts {@link geb.spock.OnFailureReporter} for use with
* {@link grails.plugin.geb.ContainerGebSpec}.
*/
@CompileStatic
class GebOnFailureReporter implements IMethodInterceptor {
Expand All @@ -37,13 +37,11 @@ class GebOnFailureReporter implements IMethodInterceptor {
} catch (IncompleteExecutionException notACauseForReporting) {
throw notACauseForReporting
} catch (Throwable throwable) {
ContainerGebSpec spec = invocation.instance as ContainerGebSpec
def spec = invocation.instance as ContainerGebSpec
if (spec.testManager.reportingEnabled) {
try {
spec.testManager.reportFailure()
} catch (ignored) {
//ignore
}
} catch (ignored) {}
}
throw throwable
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.IterationInfo

/**
* A test listener that reports the test result to {@link org.testcontainers.containers.BrowserWebDriverContainer} so
* that recordings may be saved.
* A test listener that reports the test result to
* {@link org.testcontainers.containers.BrowserWebDriverContainer}
* so that recordings may be saved.
*
* @see org.testcontainers.containers.BrowserWebDriverContainer#afterTest
*
Expand All @@ -49,17 +50,19 @@ class GebRecordingTestListener extends AbstractRunListener {
@Override
void afterIteration(IterationInfo iteration) {
try {
containerHolder.currentContainer.afterTest(
containerHolder.container.afterTest(
new ContainerGebTestDescription(iteration),
Optional.ofNullable(errorInfo?.exception)
)
} catch (NotFoundException e) {
// Handle the case where VNC recording container doesn't have a recording file
// This can happen when per-test recording is enabled and a test doesn't use the browser
if (containerHolder.grailsGebSettings.restartRecordingContainerPerTest &&
// Handle the case where VNC recording container doesn't have a recording file.
// This can happen when per-test recording is enabled and a test doesn't use the browser.
if (containerHolder.settings.restartRecordingContainerPerTest &&
e.message?.contains('/newScreen.mp4')) {
log.debug("No VNC recording found for test '{}' - this is expected for tests that don't use the browser",
iteration.displayName)
log.debug(
'No VNC recording found for test [{}] - this is expected for tests that do not use a browser',
iteration.displayName
)
} else {
// Re-throw if it's a different type of NotFoundException
throw e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ import grails.plugin.geb.support.LocalhostDownloadSupport
import grails.testing.mixin.integration.Integration

/**
* A Spock Extension that manages the Testcontainers lifecycle for a {@link grails.plugin.geb.ContainerGebSpec}
* A Spock Extension that manages the Testcontainers
* lifecycle for a {@link grails.plugin.geb.ContainerGebSpec}.
*
* <p> ContainerGebSpec cannot be a {@link geb.test.ManagedGebTest ManagedGebTest} because it would cause the test manager
* to be initialized out of sequence of the container management. Instead, we initialize the same interceptors
* as the {@link geb.spock.GebExtension GebExtension} does.
* <p>
* {@link grails.plugin.geb.ContainerGebSpec} cannot be a
* {@link geb.test.ManagedGebTest} because it would cause the test
* manager to be initialized out of sequence of the container management.
* Instead, we initialize the same interceptors as the {@link geb.spock.GebExtension} does.
*
* @author James Daugherty
* @since 4.1
Expand Down Expand Up @@ -70,6 +73,7 @@ class GrailsContainerGebExtension implements IGlobalExtension {
@Override
void visitSpec(SpecInfo spec) {
if (isContainerGebSpec(spec) && validateContainerGebSpec(spec)) {

// Do not allow parallel execution since there's only 1 set of containers in testcontainers
spec.addExclusiveResource(exclusiveResource)

Expand All @@ -78,10 +82,10 @@ class GrailsContainerGebExtension implements IGlobalExtension {
holder.reinitialize(invocation)

ContainerGebSpec gebSpec = invocation.sharedInstance as ContainerGebSpec
gebSpec.container = holder.currentContainer
gebSpec.container = holder.container
gebSpec.testManager = holder.testManager
gebSpec.downloadSupport = new LocalhostDownloadSupport(
holder.currentBrowser,
holder.browser,
holder.hostNameFromHost
)

Expand All @@ -93,7 +97,6 @@ class GrailsContainerGebExtension implements IGlobalExtension {
spec.addSetupInterceptor { invocation ->
// Grails will be initialized by this point, so setup the browser url correctly
holder.setupBrowserUrl(invocation)

invocation.proceed()
}

Expand All @@ -117,17 +120,14 @@ class GrailsContainerGebExtension implements IGlobalExtension {

addGebExtensionOnFailureReporter(spec)

GebRecordingTestListener recordingListener = new GebRecordingTestListener(
holder
)
spec.addListener(recordingListener)
spec.addListener(new GebRecordingTestListener(holder))
}
}

@TailRecursive
private boolean isContainerGebSpec(SpecInfo spec) {
if (spec != null) {
if (spec.filename.startsWith("${ContainerGebSpec.simpleName}." as String)) {
if (spec) {
if (spec.filename.startsWith("${ContainerGebSpec.simpleName}.")) {
return true
}
return isContainerGebSpec(spec.superSpec)
Expand All @@ -136,18 +136,18 @@ class GrailsContainerGebExtension implements IGlobalExtension {
}

private static boolean validateContainerGebSpec(SpecInfo specInfo) {
if (!specInfo.annotations.find { it.annotationType() == Integration }) {
throw new IllegalArgumentException('ContainerGebSpec classes must be annotated with @Integration')
if (!specInfo.annotations.any { it.annotationType() == Integration }) {
throw new IllegalArgumentException(
'ContainerGebSpec classes must be annotated with @Integration'
)
}

return true
}

private static void addGebExtensionOnFailureReporter(SpecInfo spec) {
List<MethodInfo> methods = spec.allFeatures*.featureMethod + spec.allFixtureMethods.toList()
methods.each { MethodInfo method ->
method.addInterceptor(new GebOnFailureReporter())
methods.each {
it.addInterceptor(new GebOnFailureReporter())
}
}
}

Loading
Loading