This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch 7.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/7.0.x by this push:
     new bcf4aa6f9c Geb 8 (#15067)
bcf4aa6f9c is described below

commit bcf4aa6f9cb14e3e372d40fab23a6bf06a09d694
Author: Mattias Reichel <[email protected]>
AuthorDate: Wed Sep 24 10:51:12 2025 +0200

    Geb 8 (#15067)
    
    * fix(deps): `geb` v. `7.0` -> `8.0.0`
    
    * fix: adapt to changes in Geb 8
    
    Adapt to changed driver creation logic in Geb 8.
    
    * refactor(geb): simplifications
    
    * docs(geb): improve comments and javadoc
    
    * chore(geb): whitespace
    
    * fix(geb): reset `webdriver.remote.server` after `RemoteWebDriver` init
    
    Reinstate resetting the system property after driver construction.
    
    * docs(geb): minor comment improvement
    
    * fix(geb): prevent cross-thread leakage of `webdriver.remote.server`
    
    Scope the `webdriver.remote.server` System property to the current thread
    during `RemoteWebDriver` creation, then restore it. This avoids routing
    later sessions to the wrong endpoint in parallel builds.
    
    * fix(geb): allow testing with different browsers
    
    This allows using different browsers in `GebConfig.groovy`
    A `configuredBrowser` property has to be set that matches
    the browser capabilities used to create the `RemoteWebDriver`
    to start the correct container for the browser.
    
    * chore(geb): formatting
    
    * chore(geb): formatting
    
    * refactor(geb): simplify system property override
    
    * docs(geb): improve javadoc and code comments
    
    * fix(geb): resolve trait method conflicts via explicit override
    
    * refactor(geb): simplify
    
    * chore(geb): formatting
    
    * refactor(geb): simplify
    
    * test(geb): update test label
    
    * docs(geb): add `GebConfig` instructions to README
    
    * fix(geb): make inner classes `static`
    
    * feedback(geb): rename property to `containerBrowser`
    
    * chore(geb): formatting
    
    * fix(geb): add message for missing `containerBrowser`
    
    Add a more helpful error message in the case where
    a `containerBrowser` property is missing from `GebConfig.groovy`.
    
    * test(geb): make tests more resilient in slow runners
    
    * fix(feedback): skip `containerBrowser` validation
    
    * refactor(geb): extract methods
    
    * fix(geb): move `atCheckWaiting` to CI env variable
    
    * fix(geb): feedback - remove container fallback
    
    Throw exception when `driver` and `containerBrowser`
    are mismatched in `GebConfig` instead of falling
    back to chrome.
    
    * fix(geb): system property setting of timeout values
    
    * ci: update Geb `atCheckWaiting` system property
    
    * chore(geb): formatting
    
    * fix(geb): use correct `atCheckWaiting` system property
    
    * fix(geb): feedback - use `BigDecimal` in `GrailsGebSettings`
    
    * ci: feedback - set `atCheckWaiting` in build file
---
 .github/workflows/gradle.yml                       |   1 +
 DEVELOPMENT.md                                     |   1 +
 build.gradle                                       |   1 +
 dependencies.gradle                                |   4 +-
 gradle/functional-test-config.gradle               |   5 +
 grails-doc/build.gradle                            |   2 +-
 grails-geb/README.md                               |  40 +-
 grails-geb/build.gradle                            |   4 +-
 .../grails/plugin/geb/ContainerFileDetector.groovy |   3 +-
 .../plugin/geb/ContainerGebConfiguration.groovy    |   7 +-
 .../grails/plugin/geb/ContainerGebSpec.groovy      |  13 +-
 .../plugin/geb/ContainerGebTestDescription.groovy  |   5 +-
 .../grails/plugin/geb/GebOnFailureReporter.groovy  |  10 +-
 .../plugin/geb/GebRecordingTestListener.groovy     |  19 +-
 .../plugin/geb/GrailsContainerGebExtension.groovy  |  38 +-
 .../grails/plugin/geb/GrailsGebSettings.groovy     |  84 +++--
 .../plugin/geb/WebDriverContainerHolder.groovy     | 406 ++++++++++++++-------
 grails-test-examples/geb-gebconfig/build.gradle    |   1 +
 .../groovy/org/demo/spock/GebConfigSpec.groovy     |   4 +-
 .../integration-test/resources/GebConfig.groovy    |  39 +-
 .../geb/grails-app/views/serverName/index.gsp      |   9 +-
 .../spock/ContainerFileDetectorDefaultSpec.groovy  |   7 +-
 .../spock/ContainerFileDetectorSpockSpec.groovy    |   2 +-
 .../org/demo/spock/DownloadSupportSpec.groovy      |   4 +-
 .../org/demo/spock/InheritedConfigSpec.groovy      |  16 +-
 .../groovy/org/demo/spock/PageDelegateSpec.groovy  |   2 +-
 .../org/demo/spock/PerTestRecordingSpec.groovy     |   6 +-
 .../groovy/org/demo/spock/RootPageSpec.groovy      |   4 +-
 .../org/demo/spock/ServerNameControllerSpec.groovy |   4 +-
 .../groovy/org/demo/spock/UploadSpec.groovy        |  12 +-
 .../ServerNamePage.groovy}                         |  16 +-
 .../UploadSuccessPage.groovy}                      |  15 +-
 32 files changed, 508 insertions(+), 276 deletions(-)

diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 1585dbd990..6a1fa73326 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -178,6 +178,7 @@ jobs:
           --continue
           --rerun-tasks
           --stacktrace
+          -PgebAtCheckWaiting
           -PonlyFunctionalTests
           -PskipCodeStyle
           -PskipHibernate5Tests
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 9399c7eaba..43541a0ce4 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -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
diff --git a/build.gradle b/build.gradle
index 4a96394b39..d1a9bae86c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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()
diff --git a/dependencies.gradle b/dependencies.gradle
index 0dd451e4ce..cb50888b5a 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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',
@@ -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']}",
diff --git a/gradle/functional-test-config.gradle 
b/gradle/functional-test-config.gradle
index 2ec6226495..2aa21d95a3 100644
--- a/gradle/functional-test-config.gradle
+++ b/gradle/functional-test-config.gradle
@@ -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 {
diff --git a/grails-doc/build.gradle b/grails-doc/build.gradle
index ee8d7f1474..26d675dea9 100644
--- a/grails-doc/build.gradle
+++ b/grails-doc/build.gradle
@@ -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
diff --git a/grails-geb/README.md b/grails-geb/README.md
index 29fe8c48bd..a6a1280d0a 100644
--- a/grails-geb/README.md
+++ b/grails-geb/README.md
@@ -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
@@ -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.
diff --git a/grails-geb/build.gradle b/grails-geb/build.gradle
index 857eb49b2c..eac82e74fc 100644
--- a/grails-geb/build.gradle
+++ b/grails-geb/build.gradle
@@ -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"
@@ -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 {
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerFileDetector.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerFileDetector.groovy
index ca27a04278..86fca0df62 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerFileDetector.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerFileDetector.groovy
@@ -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
  */
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy
index 1a083e6929..cd917fb966 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy
@@ -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
@@ -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
 
@@ -70,7 +71,7 @@ import org.testcontainers.containers.GenericContainer
 }
 
 /**
- * Inheritable version of {@link ContainerGebConfiguration}
+ * Inheritable version of {@link ContainerGebConfiguration}.
  *
  * @since 4.2
  */
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
index 2a1b258de9..2b6799ebc1 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
@@ -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
@@ -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>
@@ -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
+    }
 }
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy
index 237b8228ee..62b82d124a 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy
@@ -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
@@ -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
     }
 }
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebOnFailureReporter.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebOnFailureReporter.groovy
index e8f8daa65e..e7e71c885e 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebOnFailureReporter.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebOnFailureReporter.groovy
@@ -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 {
@@ -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
         }
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy
index 0b34b8424f..3deaac5822 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy
@@ -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
  *
@@ -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
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy
index 4863101815..953f48228b 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy
@@ -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
@@ -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)
 
@@ -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
                 )
 
@@ -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()
             }
 
@@ -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)
@@ -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())
         }
     }
 }
-
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy
index ee6db82106..632edc9491 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy
@@ -25,11 +25,14 @@ import groovy.transform.CompileStatic
 import groovy.transform.Memoized
 import groovy.util.logging.Slf4j
 
+import geb.waiting.Wait
+
 import static 
org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode
 import static 
org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat
 
 /**
- * Handles parsing various recording configuration used by {@link 
GrailsContainerGebExtension}
+ * Handles parsing various recording configuration
+ * used by {@link GrailsContainerGebExtension}.
  *
  * @author James Daugherty
  * @since 4.1
@@ -38,13 +41,14 @@ import static 
org.testcontainers.containers.VncRecordingContainer.VncRecordingFo
 @CompileStatic
 class GrailsGebSettings {
 
+    public static boolean DEFAULT_AT_CHECK_WAITING = false
     private static VncRecordingMode DEFAULT_RECORDING_MODE = 
VncRecordingMode.SKIP
     private static VncRecordingFormat DEFAULT_RECORDING_FORMAT = 
VncRecordingFormat.MP4
     public static int DEFAULT_TIMEOUT_IMPLICITLY_WAIT = 0
     public static int DEFAULT_TIMEOUT_PAGE_LOAD = 300
     public static int DEFAULT_TIMEOUT_SCRIPT = 30
 
-    String tracingEnabled
+    boolean tracingEnabled
     String recordingDirectoryName
     String reportingDirectoryName
     boolean restartRecordingContainerPerTest
@@ -55,8 +59,12 @@ class GrailsGebSettings {
     int pageLoadTimeout
     int scriptTimeout
 
+    boolean atCheckWaiting
+    Number timeout
+    Number retryInterval
+
     GrailsGebSettings(LocalDateTime startTime) {
-        tracingEnabled = System.getProperty('grails.geb.tracing.enabled', 
'false')
+        tracingEnabled = getBooleanProperty('grails.geb.tracing.enabled', 
false)
         recordingDirectoryName = 
System.getProperty('grails.geb.recording.directory', 
'build/gebContainer/recordings')
         reportingDirectoryName = 
System.getProperty('grails.geb.reporting.directory', 
'build/gebContainer/reports')
         recordingMode = VncRecordingMode.valueOf(
@@ -65,17 +73,48 @@ class GrailsGebSettings {
         recordingFormat = VncRecordingFormat.valueOf(
                 System.getProperty('grails.geb.recording.format', 
DEFAULT_RECORDING_FORMAT.name())
         )
-        restartRecordingContainerPerTest = 
Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartRecordingContainerPerTest',
 'true'))
+        restartRecordingContainerPerTest = getBooleanProperty(
+                'grails.geb.recording.restartRecordingContainerPerTest',
+                true
+        )
         implicitlyWait = getIntProperty('grails.geb.timeouts.implicitlyWait', 
DEFAULT_TIMEOUT_IMPLICITLY_WAIT)
         pageLoadTimeout = getIntProperty('grails.geb.timeouts.pageLoad', 
DEFAULT_TIMEOUT_PAGE_LOAD)
         scriptTimeout = getIntProperty('grails.geb.timeouts.script', 
DEFAULT_TIMEOUT_SCRIPT)
+        atCheckWaiting = 
getBooleanProperty('grails.geb.atCheckWaiting.enabled', 
DEFAULT_AT_CHECK_WAITING)
+        timeout = getNumberProperty('grails.geb.timeouts.timeout', 
Wait.DEFAULT_TIMEOUT)
+        retryInterval = getNumberProperty('grails.geb.timeouts.retryInterval', 
Wait.DEFAULT_RETRY_INTERVAL)
         this.startTime = startTime
     }
 
+    private static boolean getBooleanProperty(String propertyName, boolean 
defaultValue) {
+        Boolean.parseBoolean(System.getProperty(propertyName, 
defaultValue.toString()))
+    }
+
     private static int getIntProperty(String propertyName, int defaultValue) {
         Integer.getInteger(propertyName, defaultValue) ?: defaultValue
     }
 
+    private static Number getNumberProperty(String propertyName, Number 
defaultValue) {
+        def propValue = System.getProperty(propertyName)
+        if (propValue) {
+            try {
+                if (propValue.contains('.')) {
+                    return new BigDecimal(propValue)
+                } else {
+                    return Integer.parseInt(propValue)
+                }
+            } catch (NumberFormatException ignored) {
+                log.warn(
+                        'Could not parse property [{}] with value [{}] as a 
Number. Using default value [{}] instead.',
+                        propertyName,
+                        propValue,
+                        defaultValue
+                )
+            }
+        }
+        return defaultValue
+    }
+
     boolean isRecordingEnabled() {
         recordingMode != VncRecordingMode.SKIP
     }
@@ -85,18 +124,7 @@ class GrailsGebSettings {
         if (!recordingEnabled) {
             return null
         }
-
-        File recordingDirectory = new 
File("${recordingDirectoryName}${File.separator}${DateTimeFormatter.ofPattern('yyyyMMdd_HHmmss').format(startTime)}")
-        if (!recordingDirectory.exists()) {
-            if (!recordingDirectory.parentFile.exists()) {
-                log.info('Could not find `{}` Directory for recording. 
Creating...', recordingDirectoryName)
-            }
-            recordingDirectory.mkdirs()
-        } else if (!recordingDirectory.isDirectory()) {
-            throw new IllegalStateException("Configured recording directory 
'${recordingDirectory}' is expected to be a directory, but found file instead.")
-        }
-
-        return recordingDirectory
+        createDirectory(recordingDirectoryName, 'recording')
     }
 
     @Memoized
@@ -104,17 +132,23 @@ class GrailsGebSettings {
         if (!reportingDirectoryName) {
             return null
         }
+        createDirectory(reportingDirectoryName, 'reporting')
+    }
 
-        File reportingDirectory = new 
File("${reportingDirectoryName}${File.separator}${DateTimeFormatter.ofPattern('yyyyMMdd_HHmmss').format(startTime)}")
-        if (!reportingDirectory.exists()) {
-            if (!reportingDirectory.parentFile.exists()) {
-                log.info('Could not find `{}` Directory for reporting. 
Creating...', reportingDirectoryName)
+    private File createDirectory(String directoryName, String useCase) {
+        def dir = new File(
+                
"$directoryName$File.separator${DateTimeFormatter.ofPattern('yyyyMMdd_HHmmss').format(startTime)}"
+        )
+        if (!dir.exists()) {
+            if (!dir.parentFile.exists()) {
+                log.info('Could not find [{}] directory for {}. Creating...', 
directoryName, useCase)
             }
-            reportingDirectory.mkdirs()
-        } else if (!reportingDirectory.isDirectory()) {
-            throw new IllegalStateException("Configured reporting directory 
'${reportingDirectory}' is expected to be a directory, but found file instead.")
+            dir.mkdirs()
+        } else if (!dir.isDirectory()) {
+            throw new IllegalStateException(
+                    "Configured $useCase directory [$dir] is expected to be a 
directory, but found file instead."
+            )
         }
-
-        return reportingDirectory
+        return dir
     }
 }
diff --git 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy
 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy
index c1f21992e8..3baeee8385 100644
--- 
a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy
+++ 
b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy
@@ -18,7 +18,6 @@
  */
 package grails.plugin.geb
 
-import java.lang.reflect.Field
 import java.time.Duration
 import java.time.temporal.ChronoUnit
 import java.util.function.Supplier
@@ -27,6 +26,7 @@ import groovy.transform.CompileStatic
 import groovy.transform.EqualsAndHashCode
 import groovy.transform.PackageScope
 import groovy.util.logging.Slf4j
+import org.codehaus.groovy.runtime.InvokerHelper
 
 import com.github.dockerjava.api.model.ContainerNetwork
 import geb.Browser
@@ -34,21 +34,31 @@ import geb.Configuration
 import geb.ConfigurationLoader
 import geb.spock.SpockGebTestManagerBuilder
 import geb.test.GebTestManager
+import geb.waiting.Wait
+import org.openqa.selenium.SessionNotCreatedException
 import org.openqa.selenium.chrome.ChromeOptions
 import org.openqa.selenium.remote.RemoteWebDriver
 import org.spockframework.runtime.extension.IMethodInvocation
 import org.spockframework.runtime.model.SpecInfo
 import org.testcontainers.Testcontainers
 import org.testcontainers.containers.BrowserWebDriverContainer
+import org.testcontainers.containers.ContainerFetchException
 import org.testcontainers.containers.PortForwardingContainer
+import org.testcontainers.containers.SeleniumUtils
 import org.testcontainers.containers.VncRecordingContainer
 import org.testcontainers.images.PullPolicy
+import org.testcontainers.utility.DockerImageName
 
 import grails.plugin.geb.serviceloader.ServiceRegistry
 
+import static GrailsGebSettings.DEFAULT_AT_CHECK_WAITING
+import static GrailsGebSettings.DEFAULT_TIMEOUT_IMPLICITLY_WAIT
+import static GrailsGebSettings.DEFAULT_TIMEOUT_PAGE_LOAD
+import static GrailsGebSettings.DEFAULT_TIMEOUT_SCRIPT
+
 /**
- * Responsible for initializing a {@link 
org.testcontainers.containers.BrowserWebDriverContainer 
BrowserWebDriverContainer}
- * per the Spec's {@link grails.plugin.geb.ContainerGebConfiguration 
ContainerGebConfiguration}.  This class will try to
+ * Responsible for initializing a {@link 
org.testcontainers.containers.BrowserWebDriverContainer}
+ * per the Spec's {@link grails.plugin.geb.ContainerGebConfiguration}. This 
class will try to
  * reuse the same container if the configuration matches the current container.
  *
  * @author James Daugherty
@@ -59,47 +69,55 @@ import grails.plugin.geb.serviceloader.ServiceRegistry
 class WebDriverContainerHolder {
 
     private static final String DEFAULT_HOSTNAME_FROM_HOST = 'localhost'
+    private static final String REMOTE_ADDRESS_PROPERTY = 
'webdriver.remote.server'
+    private static final String DEFAULT_BROWSER = 'chrome'
 
-    GrailsGebSettings grailsGebSettings
+    GrailsGebSettings settings
     GebTestManager testManager
-    Browser currentBrowser
-    BrowserWebDriverContainer currentContainer
-    WebDriverContainerConfiguration currentConfiguration
+    Browser browser
+    BrowserWebDriverContainer container
+    WebDriverContainerConfiguration containerConf
 
-    WebDriverContainerHolder(GrailsGebSettings grailsGebSettings) {
-        this.grailsGebSettings = grailsGebSettings
+    WebDriverContainerHolder(GrailsGebSettings settings) {
+        this.settings = settings
     }
 
     boolean isInitialized() {
-        currentContainer != null
+        container != null
     }
 
     void stop() {
-        currentContainer?.stop()
-        currentContainer = null
-        currentBrowser = null
+        container?.stop()
+        container = null
+        browser = null
         testManager = null
-        currentConfiguration = null
+        containerConf = null
     }
 
-    boolean 
matchesCurrentContainerConfiguration(WebDriverContainerConfiguration 
specConfiguration) {
-        specConfiguration == currentConfiguration && 
grailsGebSettings.recordingMode == 
BrowserWebDriverContainer.VncRecordingMode.SKIP
+    boolean 
matchesCurrentContainerConfiguration(WebDriverContainerConfiguration specConf) {
+        specConf == containerConf &&
+        settings.recordingMode == 
BrowserWebDriverContainer.VncRecordingMode.SKIP
     }
 
-    private static int getPort(IMethodInvocation invocation) {
+    private static int findServerPort(IMethodInvocation methodInvocation) {
         try {
-            return (int) 
invocation.instance.metaClass.getProperty(invocation.instance, 'serverPort')
+            return (int) methodInvocation.instance.metaClass.getProperty(
+                    methodInvocation.instance,
+                    'serverPort'
+            )
         } catch (ignored) {
-            throw new IllegalStateException('Test class must be annotated with 
@Integration for serverPort to be injected')
+            throw new IllegalStateException(
+                    'Test class must be annotated with @Integration for 
serverPort to be injected'
+            )
         }
     }
 
     @PackageScope
-    boolean reinitialize(IMethodInvocation invocation) {
-        WebDriverContainerConfiguration specConfiguration = new 
WebDriverContainerConfiguration(
-                invocation.getSpec()
+    boolean reinitialize(IMethodInvocation methodInvocation) {
+        def specConf = new WebDriverContainerConfiguration(
+                methodInvocation.spec
         )
-        if (matchesCurrentContainerConfiguration(specConfiguration)) {
+        if (matchesCurrentContainerConfiguration(specConf)) {
             return false
         }
 
@@ -107,51 +125,73 @@ class WebDriverContainerHolder {
             stop()
         }
 
-        currentConfiguration = specConfiguration
-        currentContainer = new BrowserWebDriverContainer().withRecordingMode(
-                grailsGebSettings.recordingMode,
-                grailsGebSettings.recordingDirectory,
-                grailsGebSettings.recordingFormat
+        def gebConf = new ConfigurationLoader().conf
+        def gebConfigExists = gebConf.rawConfig.size() != 0
+        def dockerImageName = createDockerImageName(DEFAULT_BROWSER)
+        def customBrowser = gebConf.rawConfig.containerBrowser as String
+
+        if (gebConfigExists) {
+            validateDriverConf(gebConf)
+            if (customBrowser) {
+                // Prepare for creating a container matching
+                // the GebConfig `containerBrowser` property.
+                dockerImageName = createDockerImageName(customBrowser)
+            } else {
+                log.info(
+                        'No \'containerBrowser\' property found in GebConfig. 
' +
+                        "Using default [$DEFAULT_BROWSER] container image."
+                )
+            }
+        }
+
+        containerConf = specConf
+        container = new 
BrowserWebDriverContainer(dockerImageName).withRecordingMode(
+                settings.recordingMode,
+                settings.recordingDirectory,
+                settings.recordingFormat
         )
 
-        currentContainer.with {
-            withEnv('SE_ENABLE_TRACING', grailsGebSettings.tracingEnabled)
+        container.with {
+            withEnv('SE_ENABLE_TRACING', settings.tracingEnabled.toString())
             withAccessToHost(true)
             withImagePullPolicy(PullPolicy.ageBased(Duration.of(1, 
ChronoUnit.DAYS)))
-            start()
         }
+        startContainer(container, dockerImageName, customBrowser)
+
         if (hostnameChanged) {
-            currentContainer.execInContainer('/bin/sh', '-c', "echo 
'$hostIp\t${currentConfiguration.hostName}' | sudo tee -a /etc/hosts")
+            container.execInContainer(
+                    '/bin/sh', '-c',
+                    "echo '$hostIp\t$containerConf.hostName' | sudo tee -a 
/etc/hosts"
+            )
         }
 
-        // Create a Geb Configuration the same way as an empty Browser 
constructor would do
-        Configuration gebConfig = new ConfigurationLoader().conf
+        // Ensure that the driver points to the re-initialized container with 
the correct host.
+        // The driver is explicitly quit by us in stop() method, to fulfill 
our resulting responsibility.
+        gebConf.cacheDriver = false
 
-        // Ensure driver points to re-initialized container with correct host
-        // Driver is explicitly quit by us in stop() method to fulfill our 
resulting responsibility
-        gebConfig.cacheDriver = false
+        // As we don't cache, this will have been defaulted to true. We 
override to false.
+        gebConf.quitDriverOnBrowserReset = false
 
-        // "If driver caching is disabled then this setting defaults to true" 
- we override to false
-        gebConfig.quitDriverOnBrowserReset = false
-
-        gebConfig.baseUrl = currentContainer.seleniumAddress.toString()
-        if (currentConfiguration.reporting) {
-            gebConfig.reportsDir = grailsGebSettings.reportingDirectory
-            gebConfig.reporter = (invocation.sharedInstance as 
ContainerGebSpec).createReporter()
+        gebConf.baseUrl = container.seleniumAddress
+        if (containerConf.reporting) {
+            gebConf.reportsDir = settings.reportingDirectory
+            gebConf.reporter = (methodInvocation.sharedInstance as 
ContainerGebSpec).createReporter()
         }
 
-        if (gebConfig.driverConf instanceof RemoteWebDriver) {
-            // Similar to Browser#getDriverConf's Exception
-            throw new IllegalStateException(
-                    "The 'driver' config value is an instance of 
RemoteWebDriver. " +
-                            'You need to wrap the driver instance in a 
closure.'
+        if (gebConf.driverConf) {
+            // As a custom `GebConfig` cannot know the `remoteAddress` of the 
container beforehand,
+            // the `RemoteWebDriver` will be instantiated using the 
`webdriver.remote.server`
+            // system property. We set that property to inform the driver of 
the container address.
+            gebConf.driverConf = ClosureDecorators.withSystemProperty(
+                    gebConf.driverConf as Closure,
+                    REMOTE_ADDRESS_PROPERTY,
+                    container.seleniumAddress
             )
-        }
-        if (gebConfig.driverConf == null) {
-            // If no driver was set in GebConfig.groovy, default to Chrome
-            gebConfig.driverConf = { ->
-                log.info('Using default Chrome RemoteWebDriver for {}', 
currentContainer.seleniumAddress)
-                new RemoteWebDriver(currentContainer.seleniumAddress, new 
ChromeOptions().tap {
+        } else {
+            // If no driver was set in GebConfig, create a Chrome driver
+            gebConf.driverConf = { ->
+                log.info('Using default Chrome RemoteWebDriver for {}', 
container.seleniumAddress)
+                new RemoteWebDriver(container.seleniumAddress, new 
ChromeOptions().tap {
                     // See https://issues.chromium.org/issues/42323769
                     setExperimentalOption('prefs', [
                             'credentials_enable_service': false,
@@ -162,102 +202,159 @@ class WebDriverContainerHolder {
             }
         }
 
-        // If `GebConfig` instantiates a `RemoteWebDriver` without using it's 
`remoteAddress` constructor,
-        // the `RemoteWebDriver` will be instantiated using the 
`webdriver.remote.server` system property.
-        String existingPropertyValue = 
System.getProperty('webdriver.remote.server')
-        System.setProperty('webdriver.remote.server', 
currentContainer.seleniumAddress.toString())
-        gebConfig.driver // This will implicitly call `createDriver()`
-
-        // Restore the `webdriver.remote.server` system property
-        if (existingPropertyValue == null) {
-            System.clearProperty('webdriver.remote.server')
-        } else {
-            System.setProperty('webdriver.remote.server', 
existingPropertyValue)
-        }
-
-        currentBrowser = new Browser(gebConfig)
+        browser = createBrowser(gebConf)
+        applyFileDetector(browser, containerConf)
+        applyTimeouts(browser, settings)
 
-        if (currentConfiguration.fileDetector != NullContainerFileDetector) {
-            ServiceRegistry.setInstance(ContainerFileDetector, 
currentConfiguration.fileDetector)
-        }
-        ContainerFileDetector fileDetector = 
ServiceRegistry.getInstance(ContainerFileDetector, DefaultContainerFileDetector)
-        ((RemoteWebDriver) currentBrowser.driver).setFileDetector(fileDetector)
+        // There's a bit of a chicken and egg problem here: the container and 
browser are initialized
+        // when the static/shared fields are initialized, which is before the 
grails server has started
+        // so the real url cannot be set (it will be checked as part of the 
geb test manager startup in
+        // reporting mode). We set the url to localhost, which the selenium 
server should respond to
+        // (albeit with an error that will be ignored).
+        browser.baseUrl = 'http://localhost'
 
-        // Overwrite `GebConfig` timeouts with values explicitly set in 
`GrailsGebSettings` (via system properties)
-        if (grailsGebSettings.implicitlyWait != 
GrailsGebSettings.DEFAULT_TIMEOUT_IMPLICITLY_WAIT)
-            
currentBrowser.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(grailsGebSettings.implicitlyWait))
-        if (grailsGebSettings.pageLoadTimeout != 
GrailsGebSettings.DEFAULT_TIMEOUT_PAGE_LOAD)
-            
currentBrowser.driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(grailsGebSettings.pageLoadTimeout))
-        if (grailsGebSettings.scriptTimeout != 
GrailsGebSettings.DEFAULT_TIMEOUT_SCRIPT)
-            
currentBrowser.driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(grailsGebSettings.scriptTimeout))
+        testManager = createTestManager()
 
-        // There's a bit of a chicken and egg problem here: the container & 
browser are initialized when
-        // the static/shared fields are initialized, which is before the 
grails server has started so the
-        // real url cannot be set (it will be checked as part of the geb test 
manager startup in reporting mode)
-        // set the url to localhost, which the selenium server should respond 
to (albeit with an error that will be ignored)
+        return true
+    }
 
-        currentBrowser.baseUrl = 'http://localhost'
+    private static Browser createBrowser(Configuration gebConf) {
+        def browser = new Browser(gebConf)
+        try {
+            browser.driver
+        }
+        catch (SessionNotCreatedException e) {
+            throw new IllegalStateException(
+                    'Failed to create a remote browser session. ' +
+                    'Did you set a \'containerBrowser\' property ' +
+                    'corresponding to the \'driver\' in GebConfig?',
+                    e
+            )
+        }
+        browser
+    }
 
-        testManager = createTestManager()
+    private static void applyFileDetector(Browser browser, 
WebDriverContainerConfiguration conf) {
+        if (conf.fileDetector != NullContainerFileDetector) {
+            ServiceRegistry.setInstance(ContainerFileDetector, 
conf.fileDetector)
+        }
+        ((RemoteWebDriver) browser.driver).fileDetector = 
ServiceRegistry.getInstance(
+                ContainerFileDetector,
+                DefaultContainerFileDetector
+        )
+    }
 
-        return true
+    private static void applyTimeouts(Browser browser, GrailsGebSettings 
settings) {
+        // Overwrite `GebConfig` timeouts with values explicitly set in
+        // `GrailsGebSettings` (via system properties)
+        if (settings.implicitlyWait != DEFAULT_TIMEOUT_IMPLICITLY_WAIT)
+            
browser.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(settings.implicitlyWait))
+        if (settings.pageLoadTimeout != DEFAULT_TIMEOUT_PAGE_LOAD)
+            
browser.driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(settings.pageLoadTimeout))
+        if (settings.scriptTimeout != DEFAULT_TIMEOUT_SCRIPT)
+            
browser.driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(settings.scriptTimeout))
+        if (settings.atCheckWaiting != DEFAULT_AT_CHECK_WAITING)
+            browser.config.atCheckWaiting = settings.atCheckWaiting
+        if (settings.timeout != Wait.DEFAULT_TIMEOUT)
+            (browser.config.rawConfig.waiting as ConfigObject).timeout = 
settings.timeout
+        if (settings.retryInterval != Wait.DEFAULT_RETRY_INTERVAL)
+            (browser.config.rawConfig.waiting as ConfigObject).retryInterval = 
settings.retryInterval
     }
 
-    void setupBrowserUrl(IMethodInvocation invocation) {
-        if (!currentBrowser) {
-            return
+    private static void startContainer(BrowserWebDriverContainer container, 
DockerImageName dockerImageName, String customBrowser) {
+        try {
+            container.start()
+        } catch (ContainerFetchException e) {
+            if (customBrowser) {
+                throw new IllegalStateException(
+                        "Could not find the Docker image [$dockerImageName] " +
+                        "with the browser name from the 'containerBrowser' 
[$customBrowser] " +
+                        'property specified in GebConfig. ' +
+                        'See https://hub.docker.com/u/selenium for a list of 
available images.',
+                        e
+                )
+            }
+            throw e
         }
-        int port = getPort(invocation)
-        Testcontainers.exposeHostPorts(port)
+    }
 
-        currentBrowser.baseUrl = 
"${currentConfiguration.protocol}://${currentConfiguration.hostName}:${port}"
+    void setupBrowserUrl(IMethodInvocation methodInvocation) {
+        if (!browser) return
+        int hostPort = findServerPort(methodInvocation)
+        Testcontainers.exposeHostPorts(hostPort)
+        browser.baseUrl = 
"$containerConf.protocol://$containerConf.hostName:$hostPort"
     }
 
     private GebTestManager createTestManager() {
         new SpockGebTestManagerBuilder()
-                .withReportingEnabled(currentConfiguration.reporting)
+                .withReportingEnabled(containerConf.reporting)
                 .withBrowserCreator(
                         new Supplier<Browser>() {
                             @Override
                             Browser get() {
-                                currentBrowser
+                                browser
                             }
                         }
                 )
                 .build()
     }
 
-    private boolean getHostnameChanged() {
-        currentConfiguration.hostName != 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
+    private boolean isHostnameChanged() {
+        containerConf.hostName != 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
     }
 
     private static String getHostIp() {
         try {
             PortForwardingContainer.getDeclaredMethod('getNetwork').with {
                 accessible = true
-                Optional<ContainerNetwork> network = 
invoke(PortForwardingContainer.INSTANCE) as Optional<ContainerNetwork>
-                return network.get().ipAddress
+                (invoke(PortForwardingContainer.INSTANCE) as 
Optional<ContainerNetwork>)
+                        .get()
+                        .ipAddress
             }
         } catch (Exception e) {
-            throw new RuntimeException('Could not access network from 
PortForwardingContainer', e)
+            throw new RuntimeException(
+                    'Could not access network from PortForwardingContainer',
+                    e
+            )
         }
     }
 
     /**
      * Returns the hostname that the server under test is available on from 
the host.
-     * <p>This is useful when using any of the {@code download*()} methods as 
they will connect from the host,
-     * and not from within the container.
+     * <p>This is useful when using any of the {@code download*()} methods as 
they will
+     * connect from the host, and not from within the container.
+     *
      * <p>Defaults to {@code localhost}. If the value returned by {@code 
webDriverContainer.getHost()}
-     * is different from the default, this method will return the same value 
same as {@code webDriverContainer.getHost()}.
+     * is different from the default, this method will return the same value 
same as
+     * {@code webDriverContainer.getHost()}.
      *
      * @return the hostname for accessing the server under test from the host
      */
     String getHostNameFromHost() {
-        return hostNameChanged ? currentContainer.host : 
DEFAULT_HOSTNAME_FROM_HOST
+        hostNameChanged ? container.host : DEFAULT_HOSTNAME_FROM_HOST
     }
 
     private boolean isHostNameChanged() {
-        return currentContainer.host != 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
+        container.host != 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
+    }
+
+    private static DockerImageName createDockerImageName(Object browserName) {
+        DockerImageName.parse(
+                "selenium/standalone-$browserName:$seleniumVersion"
+        )
+    }
+
+    private static void validateDriverConf(Configuration gebConf) {
+        if (gebConf.driverConf && !(gebConf.driverConf instanceof Closure)) {
+            throw new IllegalStateException(
+                    'The \'driver\' property of GebConfig must be a ' +
+                    'Closure that returns an instance of RemoteWebDriver.'
+            )
+        }
+    }
+
+    private static String getSeleniumVersion() {
+        SeleniumUtils.determineClasspathSeleniumVersion()
     }
 
     @CompileStatic
@@ -270,58 +367,117 @@ class WebDriverContainerHolder {
         Class<? extends ContainerFileDetector> fileDetector
 
         WebDriverContainerConfiguration(SpecInfo spec) {
-            ContainerGebConfiguration configuration
+
+            ContainerGebConfiguration conf
 
             // Check if the class implements the interface
             if (IContainerGebConfiguration.isAssignableFrom(spec.reflection)) {
-                configuration = spec.reflection.getConstructor().newInstance() 
as ContainerGebConfiguration
+                conf = spec.reflection.getConstructor().newInstance() as 
ContainerGebConfiguration
             } else {
                 // Check for the annotation
-                configuration = spec.annotations.find {
+                conf = spec.annotations.find {
                     it.annotationType() == ContainerGebConfiguration
                 } as ContainerGebConfiguration
             }
 
-            protocol = configuration?.protocol() ?: 
ContainerGebConfiguration.DEFAULT_PROTOCOL
-            hostName = configuration?.hostName() ?: 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
-            reporting = configuration?.reporting() ?: false
-            fileDetector = configuration?.fileDetector() ?: 
ContainerGebConfiguration.DEFAULT_FILE_DETECTOR
+            protocol = conf?.protocol() ?: 
ContainerGebConfiguration.DEFAULT_PROTOCOL
+            hostName = conf?.hostName() ?: 
ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
+            reporting = conf?.reporting() ?: false
+            fileDetector = conf?.fileDetector() ?: 
ContainerGebConfiguration.DEFAULT_FILE_DETECTOR
         }
     }
 
     /**
      * Workaround for 
https://github.com/testcontainers/testcontainers-java/issues/3998
-     * Restarts the VNC recording container to enable separate recording files 
for each test method.
-     * This method uses reflection to access the VNC recording container field 
in BrowserWebDriverContainer.
-     * Should be called BEFORE each test starts.
+     * <p>
+     * Restarts the VNC recording container to enable separate recording files 
for each
+     * test method. This method uses reflection to access the VNC recording 
container
+     * field in BrowserWebDriverContainer. Should be called BEFORE each test 
starts.
      */
     void restartVncRecordingContainer() {
-        if (!grailsGebSettings.recordingEnabled || 
!grailsGebSettings.restartRecordingContainerPerTest || !currentContainer) {
+        if (!settings.recordingEnabled || 
!settings.restartRecordingContainerPerTest || !container) {
             return
         }
         try {
             // Use reflection to access the VNC recording container field
-            Field vncRecordingContainerField = 
BrowserWebDriverContainer.getDeclaredField('vncRecordingContainer')
-            vncRecordingContainerField.setAccessible(true)
-
-            VncRecordingContainer vncContainer = 
vncRecordingContainerField.get(currentContainer) as VncRecordingContainer
+            def field = 
BrowserWebDriverContainer.getDeclaredField('vncRecordingContainer').tap {
+                accessible = true
+            }
 
+            def vncContainer = field.get(container) as VncRecordingContainer
             if (vncContainer) {
                 // Stop the current VNC recording container
                 vncContainer.stop()
                 // Create and start a new VNC recording container for the next 
test
-                VncRecordingContainer newVncContainer = new 
VncRecordingContainer(currentContainer)
+                def newVncContainer = new VncRecordingContainer(container)
                         .withVncPassword('secret')
                         .withVncPort(5900)
-                        .withVideoFormat(grailsGebSettings.recordingFormat)
-                vncRecordingContainerField.set(currentContainer, 
newVncContainer)
+                        .withVideoFormat(settings.recordingFormat)
+                field.set(container, newVncContainer)
                 newVncContainer.start()
 
                 log.debug('Successfully restarted VNC recording container')
             }
         } catch (Exception e) {
-            log.warn("Failed to restart VNC recording container: 
${e.message}", e)
+            log.warn("Failed to restart VNC recording container: $e.message", 
e)
             // Don't throw the exception to avoid breaking the test execution
         }
     }
+
+    @CompileStatic
+    private static class ClosureDecorators {
+
+        /**
+         * Wraps a closure so that during its execution, 
System.getProperty(key)
+         * returns a custom value instead of what is actually in the system 
properties.
+         */
+        static Closure withSystemProperty(Closure target, String key, Object 
value) {
+            Closure wrapped = { Object... args ->
+                SysPropScope.withProperty(key, value.toString()) {
+                    InvokerHelper.invokeClosure(target, args)
+                }
+            }
+
+            // keep original closure semantics
+            wrapped.rehydrate(target.delegate, target.owner, 
target.thisObject).tap {
+                resolveStrategy = target.resolveStrategy
+            }
+        }
+
+        @CompileStatic
+        private static class SysPropScope {
+
+            private static final ThreadLocal<Map<String,String>> 
OVERRIDDEN_SYSTEM_PROPERTIES =
+                    ThreadLocal.withInitial { [:] as Map<String,String> }
+
+            @Lazy // Thread-safe wrapping of system properties
+            private static Properties propertiesWrappedOnFirstAccess = {
+                new InterceptingProperties().tap {
+                    putAll(System.getProperties())
+                    System.setProperties(it)
+                }
+            }()
+
+            static <T> T withProperty(String key, String value, Closure<T> 
body) {
+                propertiesWrappedOnFirstAccess // Access property to trigger 
property wrapping
+                def map = OVERRIDDEN_SYSTEM_PROPERTIES.get()
+                def prev = map.put(key, value)
+                try {
+                    return body.call()
+                } finally {
+                    if (prev == null) map.remove(key) else map[key] = prev
+                    if (map.isEmpty()) OVERRIDDEN_SYSTEM_PROPERTIES.remove()
+                }
+            }
+
+            @CompileStatic
+            private static class InterceptingProperties extends Properties {
+                @Override
+                String getProperty(String key) {
+                    def v = OVERRIDDEN_SYSTEM_PROPERTIES.get().get(key)
+                    v != null ? v : super.getProperty(key)
+                }
+            }
+        }
+    }
 }
diff --git a/grails-test-examples/geb-gebconfig/build.gradle 
b/grails-test-examples/geb-gebconfig/build.gradle
index 19782d3db2..be441fb3c6 100644
--- a/grails-test-examples/geb-gebconfig/build.gradle
+++ b/grails-test-examples/geb-gebconfig/build.gradle
@@ -67,6 +67,7 @@ dependencies {
     testImplementation 'org.spockframework:spock-core'
 
     integrationTestImplementation testFixtures('org.apache.grails:grails-geb')
+    integrationTestImplementation 
'org.seleniumhq.selenium:selenium-firefox-driver'
 }
 
 //tasks.withType(Test).configureEach {
diff --git 
a/grails-test-examples/geb-gebconfig/src/integration-test/groovy/org/demo/spock/GebConfigSpec.groovy
 
b/grails-test-examples/geb-gebconfig/src/integration-test/groovy/org/demo/spock/GebConfigSpec.groovy
index 782bd9ccfe..830a716dea 100644
--- 
a/grails-test-examples/geb-gebconfig/src/integration-test/groovy/org/demo/spock/GebConfigSpec.groovy
+++ 
b/grails-test-examples/geb-gebconfig/src/integration-test/groovy/org/demo/spock/GebConfigSpec.groovy
@@ -42,8 +42,8 @@ class GebConfigSpec extends ContainerGebSpec {
         then: 'our custom capability set in GebConfig is available'
         capabilities.getCapability('grails:gebConfigUsed') == true
 
-        and: 'the driver should have Chrome-specific capabilities'
-        capabilities.browserName == 'chrome'
+        and: 'the driver should have Firefox-specific capabilities'
+        capabilities.browserName == 'firefox'
 
         when: 'navigating to a page'
         to(HomePage)
diff --git 
a/grails-test-examples/geb-gebconfig/src/integration-test/resources/GebConfig.groovy
 
b/grails-test-examples/geb-gebconfig/src/integration-test/resources/GebConfig.groovy
index fe22ed89f8..963250f647 100644
--- 
a/grails-test-examples/geb-gebconfig/src/integration-test/resources/GebConfig.groovy
+++ 
b/grails-test-examples/geb-gebconfig/src/integration-test/resources/GebConfig.groovy
@@ -19,39 +19,32 @@
 
 import geb.report.ReportState
 import geb.report.Reporter
-import org.openqa.selenium.chrome.ChromeOptions
+import org.openqa.selenium.firefox.FirefoxOptions
 import org.openqa.selenium.remote.RemoteWebDriver
 import geb.report.ReportingListener
 
-// Configuration for container-based Geb testing
-// This driver configuration will be used by WebDriverContainerHolder
+// Configuration for container-based Geb testing.
+// This driver configuration will be used by WebDriverContainerHolder.
 driver = {
-    // Chrome preferences to disable password manager and credentials service
-    def prefs = [
-            'credentials_enable_service': false,
-            'profile.password_manager_enabled': false,
-            'profile.password_manager_leak_detection': false
-    ]
 
-    def chromeOptions = new ChromeOptions()
-    // TO DO: guest would be preferred, but this causes issues with downloads
-    // see https://issues.chromium.org/issues/42323769
-    // chromeOptions.addArguments('--guest')
-    chromeOptions.setExperimentalOption('prefs', prefs)
-
-    // Add a custom capability that we can test for to verify our 
configuration is being used
-    chromeOptions.setCapability('grails:gebConfigUsed', true)
-
-    // The remote address will be set by WebDriverContainerHolder via system 
property
-    // webdriver.remote.server before this closure is called
-    new RemoteWebDriver(chromeOptions)
+    // The remote address will be set by WebDriverContainerHolder via
+    // system property `webdriver.remote.server` before this closure is called.
+    new RemoteWebDriver(new FirefoxOptions().tap {
+        // Add a custom capability that we can test for
+        // to verify our configuration is being used.
+        setCapability('grails:gebConfigUsed', true)
+    })
 }
 
-// Another proof that GebConfig.groovy is being utilized, next to GebConfigSpec
+// The `containerBrowser` property must match the configured
+// driver in order to start up a matching Selenium browser container.
+containerBrowser = 'firefox'
+
+// Another proof that GebConfig.groovy is being utilized, next to 
GebConfigSpec.
 reportingListener = new ReportingListener() {
     void onReport(Reporter reporter, ReportState reportState, List<File> 
reportFiles) {
         reportFiles.each {
             println "[[ATTACHMENT|$it.absolutePath]]"
         }
     }
-}
\ No newline at end of file
+}
diff --git a/grails-test-examples/geb/grails-app/views/serverName/index.gsp 
b/grails-test-examples/geb/grails-app/views/serverName/index.gsp
index c90faadae3..f965987913 100644
--- a/grails-test-examples/geb/grails-app/views/serverName/index.gsp
+++ b/grails-test-examples/geb/grails-app/views/serverName/index.gsp
@@ -16,17 +16,10 @@
   ~  specific language governing permissions and limitations
   ~  under the License.
   --%>
-<%--
-  Created by IntelliJ IDEA.
-  User: sbglasius
-  Date: 25/11/2024
-  Time: 08.09
---%>
-
 <%@ page contentType="text/html;charset=UTF-8" %>
 <html>
 <head>
-    <title></title>
+    <title>Server name test</title>
 </head>
 
 <body>
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorDefaultSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorDefaultSpec.groovy
index 88586b5417..f6bf82e281 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorDefaultSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorDefaultSpec.groovy
@@ -19,6 +19,8 @@
 
 package org.demo.spock
 
+import org.demo.spock.pages.UploadSuccessPage
+
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 import org.demo.spock.pages.UploadPage
@@ -33,7 +35,7 @@ class ContainerFileDetectorDefaultSpec extends 
ContainerGebSpec {
 
     void 'should be able to find and upload local files'() {
         given:
-        def uploadPage = to UploadPage
+        def uploadPage = to(UploadPage)
 
         when:
         uploadPage.fileInput.file = new 
File('src/integration-test/resources/assets/upload-test.txt')
@@ -42,7 +44,6 @@ class ContainerFileDetectorDefaultSpec extends 
ContainerGebSpec {
         uploadPage.submitBtn.click()
 
         then:
-        title == 'File Uploaded'
-        pageSource.contains('File uploaded successfully')
+        at(UploadSuccessPage)
     }
 }
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorSpockSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorSpockSpec.groovy
index 7754097cb1..7382c31753 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorSpockSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ContainerFileDetectorSpockSpec.groovy
@@ -45,7 +45,7 @@ class ContainerFileDetectorSpockSpec extends ContainerGebSpec 
{
     @PendingFeature(reason = 
'https://github.com/apache/grails-geb/pull/146#issuecomment-2691433277')
     void 'should fail to find file with fileDetector changed to 
UselessContainerFileDetector in setupSpec'() {
         given:
-        def uploadPage = to UploadPage
+        def uploadPage = to(UploadPage)
 
         when:
         uploadPage.fileInput.file = new 
File('src/integration-test/resources/assets/upload-test.txt')
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
index 1c3d6da5bc..1e9241f080 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
@@ -19,6 +19,8 @@
 
 package org.demo.spock
 
+import org.demo.spock.pages.HomePage
+
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 
@@ -27,7 +29,7 @@ class DownloadSupportSpec extends ContainerGebSpec {
 
     void 'should be able to use download methods'() {
         when:
-        go('/')
+        to(HomePage)
 
         then:
         downloadText().contains('Welcome to Grails')
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/InheritedConfigSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/InheritedConfigSpec.groovy
index 970a6a6f9b..b90eec0a09 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/InheritedConfigSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/InheritedConfigSpec.groovy
@@ -19,6 +19,8 @@
 
 package org.demo.spock
 
+import org.demo.spock.pages.ServerNamePage
+
 import grails.plugin.geb.ContainerGebConfiguration
 import grails.plugin.geb.IContainerGebConfiguration
 import grails.plugin.geb.ContainerGebSpec
@@ -31,7 +33,7 @@ import grails.testing.mixin.integration.Integration
 class SuperSpec extends ContainerGebSpec implements IContainerGebConfiguration 
{
     @Override
     String hostName() {
-        return 'super.example.com'
+        'super.example.com'
     }
 }
 
@@ -43,7 +45,7 @@ class NotSuperSpec extends ContainerGebSpec {}
 class InheritedConfigSpec extends SuperSpec {
     void 'should show the right server name when visiting /serverName'() {
         when: 'visiting the server name controller'
-        go('/serverName')
+        to(ServerNamePage)
 
         then: 'the emitted hostname is correct'
         pageSource.contains('Server name: super.example.com')
@@ -54,7 +56,7 @@ class InheritedConfigSpec extends SuperSpec {
 class NotInheritedConfigSpec extends NotSuperSpec {
     void 'should show the right server name when visiting /serverName'() {
         when: 'visiting the server name controller'
-        go('/serverName')
+        to(ServerNamePage)
 
         then: 'the emitted hostname is correct'
         !pageSource.contains('Server name: not.example.com')
@@ -65,12 +67,12 @@ class NotInheritedConfigSpec extends NotSuperSpec {
 class ChildPreferenceInheritedConfigSpec extends SuperSpec {
     @Override
     String hostName() {
-        return 'child.example.com'
+        'child.example.com'
     }
 
     void 'should show the right server name when visiting /serverName'() {
         when: 'visiting the server name controller'
-        go('/serverName')
+        to(ServerNamePage)
 
         then: 'the emitted hostname is correct'
         pageSource.contains('Server name: child.example.com')
@@ -90,7 +92,7 @@ class ChildPreferenceInheritedConfigSpec extends SuperSpec {
 class SuperSuperInheritedConfigSpec extends SuperSpec {
     @Override
     boolean reporting() {
-        return true
+        true
     }
 }
 
@@ -98,7 +100,7 @@ class SuperSuperInheritedConfigSpec extends SuperSpec {
 class MultipleInheritanceSpec extends SuperSuperInheritedConfigSpec {
     void 'should show the right server name when visiting /serverName'() {
         when: 'visiting the server name controller'
-        go('/serverName')
+        to(ServerNamePage)
 
         then: 'the emitted hostname is correct'
         pageSource.contains('Server name: super.example.com')
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PageDelegateSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PageDelegateSpec.groovy
index 5a8eb3ec7d..ac48153be3 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PageDelegateSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PageDelegateSpec.groovy
@@ -28,7 +28,7 @@ class PageDelegateSpec extends ContainerGebSpec {
 
     void 'should delegate to page object'() {
         given:
-        to UploadPage
+        to(UploadPage)
 
         when:
         nop()
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy
index 8f82cdaafe..c60ab7a952 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy
@@ -33,7 +33,7 @@ class PerTestRecordingSpec extends ContainerGebSpec {
 
     void '(setup) running a test to create a recording'() {
         when: 'visiting the home page'
-        to HomePage
+        to(HomePage)
 
         then: 'the page loads correctly'
         title == 'Welcome to Grails'
@@ -41,7 +41,7 @@ class PerTestRecordingSpec extends ContainerGebSpec {
 
     void '(setup) running a second test to create another recording'() {
         when: 'visiting another page than the previous test'
-        to UploadPage
+        to(UploadPage)
         
         and: 'pausing to ensure the recorded file size is different'
         Thread.sleep(1000)
@@ -98,6 +98,6 @@ class PerTestRecordingSpec extends ContainerGebSpec {
     }
 
     private static boolean isVideoFile(File file) {
-        return file.isFile() && (file.name.endsWith('.mp4') || 
file.name.endsWith('.flv'))
+        file.isFile() && (file.name.endsWith('.mp4') || 
file.name.endsWith('.flv'))
     }
 }
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/RootPageSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/RootPageSpec.groovy
index 7c7cdc977a..e09cd02f22 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/RootPageSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/RootPageSpec.groovy
@@ -22,6 +22,8 @@ package org.demo.spock
 import geb.report.CompositeReporter
 import geb.report.PageSourceReporter
 import geb.report.Reporter
+import org.demo.spock.pages.HomePage
+
 import grails.plugin.geb.ContainerGebConfiguration
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
@@ -42,7 +44,7 @@ class RootPageSpec extends ContainerGebSpec {
 
     void 'should display the correct title on the home page'() {
         when: 'visiting the home page'
-        go('/')
+        to(HomePage)
 
         then: 'the page title is correct'
         report('root page report')
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ServerNameControllerSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ServerNameControllerSpec.groovy
index 691a1a03f8..042637c011 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ServerNameControllerSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/ServerNameControllerSpec.groovy
@@ -19,6 +19,8 @@
 
 package org.demo.spock
 
+import org.demo.spock.pages.ServerNamePage
+
 import grails.plugin.geb.ContainerGebConfiguration
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
@@ -33,7 +35,7 @@ class ServerNameControllerSpec extends ContainerGebSpec {
 
     void 'should show the right server name when visiting /serverName'() {
         when: 'visiting the server name controller'
-        go('/serverName')
+        to(ServerNamePage)
 
         then: 'the emitted hostname is correct'
         $('p').text() == 'Server name: testing.example.com'
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy
index 982e52846c..41c61aa577 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy
@@ -19,6 +19,8 @@
 
 package org.demo.spock
 
+import org.demo.spock.pages.UploadSuccessPage
+
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 import org.demo.spock.pages.UploadPage
@@ -31,7 +33,7 @@ class UploadSpec extends ContainerGebSpec {
     @Requires({ os.windows })
     void 'should be able to upload files on a Windows host'() {
         given:
-        def uploadPage = to UploadPage
+        def uploadPage = to(UploadPage)
 
         when:
         uploadPage.fileInput.file = createFileInputSource(
@@ -43,14 +45,13 @@ class UploadSpec extends ContainerGebSpec {
         uploadPage.submitBtn.click()
 
         then:
-        title == 'File Uploaded'
-        pageSource.contains('File uploaded successfully')
+        at(UploadSuccessPage)
     }
 
     @IgnoreIf({ os.windows })
     void 'should be able to upload files on a non-Windows host'() {
         given:
-        def uploadPage = to UploadPage
+        def uploadPage = to(UploadPage)
 
         when:
         uploadPage.fileInput.file = createFileInputSource(
@@ -62,7 +63,6 @@ class UploadSpec extends ContainerGebSpec {
         uploadPage.submitBtn.click()
 
         then:
-        title == 'File Uploaded'
-        pageSource.contains('File uploaded successfully')
+        at(UploadSuccessPage)
     }
 }
\ No newline at end of file
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/ServerNamePage.groovy
similarity index 70%
copy from 
grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
copy to 
grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/ServerNamePage.groovy
index 1c3d6da5bc..ed7c39f76f 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/ServerNamePage.groovy
@@ -17,19 +17,13 @@
  *  under the License.
  */
 
-package org.demo.spock
+package org.demo.spock.pages
 
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
+import geb.Page
 
-@Integration
-class DownloadSupportSpec extends ContainerGebSpec {
+class ServerNamePage extends Page {
 
-    void 'should be able to use download methods'() {
-        when:
-        go('/')
+    static url = '/serverName'
+    static at = { title == 'Server name test' }
 
-        then:
-        downloadText().contains('Welcome to Grails')
-    }
 }
\ No newline at end of file
diff --git 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/UploadSuccessPage.groovy
similarity index 70%
copy from 
grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
copy to 
grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/UploadSuccessPage.groovy
index 1c3d6da5bc..0217ff5e6e 100644
--- 
a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/DownloadSupportSpec.groovy
+++ 
b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/UploadSuccessPage.groovy
@@ -17,19 +17,12 @@
  *  under the License.
  */
 
-package org.demo.spock
+package org.demo.spock.pages
 
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
+import geb.Page
 
-@Integration
-class DownloadSupportSpec extends ContainerGebSpec {
+class UploadSuccessPage extends Page {
 
-    void 'should be able to use download methods'() {
-        when:
-        go('/')
+    static at = { title == 'File Uploaded' }
 
-        then:
-        downloadText().contains('Welcome to Grails')
-    }
 }
\ No newline at end of file

Reply via email to