This is an automated email from the ASF dual-hosted git repository. jonnybot pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy-geb.git
commit 471539a44db514ec088186faef0060179fcabbaa Author: Jonny Carter <[email protected]> AuthorDate: Tue Mar 24 10:20:01 2026 -0500 Refactor test containers to use base class pattern --- integration/geb-testcontainers/README.md | 2 +- .../ContainerFileDetectorAnnotationSpec.groovy | 8 +- .../ContainerFileDetectorSpockSpec.groovy | 15 +- .../HostNameConfigurationSpec.groovy | 10 +- .../geb/testcontainers/InheritedConfigSpec.groovy | 18 +- .../geb/testcontainers/PerTestRecordingSpec.groovy | 2 +- .../groovy/geb/testcontainers/RootPageSpec.groovy | 10 +- .../geb/testcontainers/TestFileServer.groovy | 2 +- .../ContainerGebConfiguration.groovy | 76 ++-- .../testcontainers/ContainerGebExtension.groovy | 89 ++--- .../geb/testcontainers/ContainerGebSpec.groovy | 77 +++- .../DefaultContainerFileDetector.groovy | 8 - .../geb/testcontainers/GebContainerSettings.groovy | 78 +--- .../geb/testcontainers/GebOnFailureReporter.groovy | 2 +- .../testcontainers/GebRecordingTestListener.groovy | 2 +- .../testcontainers/WebDriverContainerHolder.groovy | 288 ++++---------- .../serviceloader/ServiceRegistry.groovy | 96 ----- .../geb/testcontainers/support/BrowserType.groovy | 42 -- .../testcontainers/support/ContainerSupport.groovy | 82 ---- .../testcontainers/support/ReportingSupport.groovy | 50 --- .../support/delegate/BrowserDelegate.groovy | 429 --------------------- .../delegate/DownloadSupportDelegate.groovy | 173 --------- .../support/delegate/DriverDelegate.groovy | 53 --- .../support/delegate/PageDelegate.groovy | 415 -------------------- ...ockframework.runtime.extension.IGlobalExtension | 2 +- 25 files changed, 256 insertions(+), 1773 deletions(-) diff --git a/integration/geb-testcontainers/README.md b/integration/geb-testcontainers/README.md index aa5a3935..18dc8c6a 100644 --- a/integration/geb-testcontainers/README.md +++ b/integration/geb-testcontainers/README.md @@ -129,7 +129,7 @@ For this reason, this plugin will setup a Local File Detector by default. To customize the default, either: 1. Create a class that implements [`ContainerFileDetector`](./src/testFixtures/groovy/grails/plugin/geb/ContainerFileDetector.groovy) - and specify its fully qualified class name in a `META-INF/services/grails.plugin.geb.ContainerFileDetector` file + and specify its fully qualified class name in a `META-INF/services/geb.testcontainers.ContainerFileDetector` file on the classpath (e.g., `src/integration-test/resources`). 2. Use the `ContainerGebConfiguration` annotation and set its `fileDetector` property to your `ContainerFileDetector` implementation class. diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorAnnotationSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorAnnotationSpec.groovy index 040ef620..881e3c1b 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorAnnotationSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorAnnotationSpec.groovy @@ -25,10 +25,14 @@ import org.openqa.selenium.WebDriverException * Altered copy of {@link ContainerFileDetectorDefaultSpec} * that throws {@link org.openqa.selenium.InvalidArgumentException} */ -@ContainerGebConfiguration(fileDetector = UselessContainerFileDetector) class ContainerFileDetectorAnnotationSpec extends ContainerGebSpecWithServer { - def "should fail to find file with fileDetector changed to UselessContainerFileDetector via annotation"() { + @Override + Class<? extends ContainerFileDetector> fileDetector() { + UselessContainerFileDetector + } + + def "should fail to find file with fileDetector changed to UselessContainerFileDetector via interface"() { given: def uploadPage = to UploadPage diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorSpockSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorSpockSpec.groovy index 02267980..9212d12e 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorSpockSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/ContainerFileDetectorSpockSpec.groovy @@ -18,26 +18,21 @@ */ package geb.testcontainers -import geb.testcontainers.serviceloader.ServiceRegistry import geb.testcontainers.pages.UploadPage import org.openqa.selenium.WebDriverException -import spock.lang.PendingFeature /** * Altered copy of {@link ContainerFileDetectorAnnotationSpec} + * that configures the file detector via the interface. */ class ContainerFileDetectorSpockSpec extends ContainerGebSpecWithServer { - def setupSpec() { - ServiceRegistry.setInstance(ContainerFileDetector, new UselessContainerFileDetector()) + @Override + Class<? extends ContainerFileDetector> fileDetector() { + UselessContainerFileDetector } - def cleanupSpec() { - ServiceRegistry.setInstance(ContainerFileDetector, null) - } - - @PendingFeature(reason = 'https://github.com/apache/grails-geb/pull/146#issuecomment-2691433277') - def "should fail to find file with fileDetector changed to UselessContainerFileDetector in setupSpec"() { + def "should fail to find file with fileDetector changed to UselessContainerFileDetector via interface"() { given: def uploadPage = to(UploadPage) diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/HostNameConfigurationSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/HostNameConfigurationSpec.groovy index 1b3b7a7b..9e6fae98 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/HostNameConfigurationSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/HostNameConfigurationSpec.groovy @@ -22,13 +22,17 @@ import geb.testcontainers.pages.HomePage import spock.lang.Title /** - * See https://grails.apache.org/docs/latest/guide/testing.html#functionalTesting and https://groovy.apache.org/geb/manual/current/ - * for more instructions on how to write functional tests with Grails and Geb. + * See https://groovy.apache.org/geb/manual/current/ + * for more instructions on how to write functional tests with Geb. */ @Title("host name configuration test") -@ContainerGebConfiguration(hostName = 'testing.example.com') class HostNameConfigurationSpec extends ContainerGebSpecWithServer { + @Override + String hostName() { + 'testing.example.com' + } + def "should show the right server name when visiting home page"() { when: "visiting the hpme page with a configured host name" to(HomePage) diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/InheritedConfigSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/InheritedConfigSpec.groovy index 764f3457..cd73cb78 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/InheritedConfigSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/InheritedConfigSpec.groovy @@ -23,15 +23,19 @@ import geb.testcontainers.pages.HomePage /** * Adaptation of {@link HostNameConfigurationSpec} */ -class SuperSpec extends ContainerGebSpecWithServer implements IContainerGebConfiguration { +class SuperSpec extends ContainerGebSpecWithServer { @Override String hostName() { 'super.example.com' } } -@ContainerGebConfiguration(hostName = 'not.example.com') -class NotSuperSpec extends ContainerGebSpecWithServer {} +class NotSuperSpec extends ContainerGebSpecWithServer { + @Override + String hostName() { + 'not.example.com' + } +} class InheritedConfigSpec extends SuperSpec { void 'should show the right server name when visiting /serverName'() { @@ -39,7 +43,6 @@ class InheritedConfigSpec extends SuperSpec { to(HomePage) then: 'the emitted hostname is correct' - // pageSource.contains('Server name: super.example.com') currentUrl == "http://super.example.com:8090/" } @@ -54,9 +57,8 @@ class NotInheritedConfigSpec extends NotSuperSpec { when: 'visiting the server name controller' to(HomePage) - then: 'the emitted hostname is correct' - // !pageSource.contains('Server name: not.example.com') - currentUrl != "http://not.example.com:8090/" + then: 'the emitted hostname is inherited via interface' + currentUrl == "http://not.example.com:8090/" } def cleanup() { @@ -75,7 +77,6 @@ class ChildPreferenceInheritedConfigSpec extends SuperSpec { to(HomePage) then: 'the emitted hostname is correct' - // pageSource.contains('Server name: child.example.com') currentUrl == "http://child.example.com:8090/" when: @@ -106,7 +107,6 @@ class MultipleInheritanceSpec extends SuperSuperInheritedConfigSpec { to(HomePage) then: 'the emitted hostname is correct' - // pageSource.contains('Server name: super.example.com') currentUrl == "http://super.example.com:8090/" report('multi inheritance report') } diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/PerTestRecordingSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/PerTestRecordingSpec.groovy index 2245adfd..292ea23b 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/PerTestRecordingSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/PerTestRecordingSpec.groovy @@ -95,4 +95,4 @@ class PerTestRecordingSpec extends ContainerGebSpec { return file.isFile() && (file.name.endsWith('.mp4') || file.name.endsWith('.flv')) } -} \ No newline at end of file +} diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/RootPageSpec.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/RootPageSpec.groovy index 371d5144..9789242a 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/RootPageSpec.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/RootPageSpec.groovy @@ -24,12 +24,16 @@ import geb.report.Reporter import geb.testcontainers.pages.HomePage /** - * See https://grails.apache.org/docs/latest/guide/testing.html#functionalTesting and https://groovy.apache.org/geb/manual/current/ - * for more instructions on how to write functional tests with Grails and Geb. + * See https://groovy.apache.org/geb/manual/current/ + * for more instructions on how to write functional tests with Geb. */ -@ContainerGebConfiguration(reporting = true) class RootPageSpec extends ContainerGebSpecWithServer { + @Override + boolean reporting() { + true + } + @Override Reporter createReporter() { // Override the default reporter to demonstrate how this can be customized diff --git a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/TestFileServer.groovy b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/TestFileServer.groovy index 38f01bb4..fee1d509 100644 --- a/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/TestFileServer.groovy +++ b/integration/geb-testcontainers/src/integration-test/groovy/geb/testcontainers/TestFileServer.groovy @@ -45,7 +45,7 @@ class TestFileServer { // Use JDK's built-in SimpleFileServer to serve the static content server = SimpleFileServer.createFileServer(addr, staticDirPath, OutputLevel.INFO) server.start() - println "TestFileServer started on port 8080" + println "TestFileServer started on port $port" } void stop() { diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebConfiguration.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebConfiguration.groovy index a0b72c4f..b5f6b985 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebConfiguration.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebConfiguration.groovy @@ -20,76 +20,62 @@ package geb.testcontainers import org.testcontainers.containers.GenericContainer -import java.lang.annotation.ElementType -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy -import java.lang.annotation.Target - /** - * Can be used to configure the protocol and hostname that the container's browser will use. + * Implement this interface on a {@link ContainerGebSpec} subclass to configure the + * protocol, hostname, reporting, and file detector for container-based browser tests. + * + * <p>Configuration is inherited by subclasses, and can be overridden by re-implementing + * the desired methods. + * + * <p>Example: + * <pre><code> + * class MySpec extends ContainerGebSpec { + * @Override + * String hostName() { 'testing.example.com' } + * + * @Override + * boolean reporting() { true } + * } + * </code></pre> * * @author James Daugherty * @since 4.1 */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@interface ContainerGebConfiguration { - - static final String DEFAULT_HOSTNAME_FROM_CONTAINER = GenericContainer.INTERNAL_HOST_HOSTNAME - static final String DEFAULT_PROTOCOL = 'http' - static final Class<? extends ContainerFileDetector> DEFAULT_FILE_DETECTOR = DefaultContainerFileDetector +interface ContainerGebConfiguration { /** * The protocol that the container's browser will use to access the server under test. * <p>Defaults to {@code http}. */ - String protocol() default DEFAULT_PROTOCOL + default String protocol() { + 'http' + } /** * The hostname that the container's browser will use to access the server under test. * <p>Defaults to {@code host.testcontainers.internal}. * <p>This is useful when the server under test needs to be accessed with a certain hostname. */ - String hostName() default DEFAULT_HOSTNAME_FROM_CONTAINER + default String hostName() { + GenericContainer.INTERNAL_HOST_HOSTNAME + } /** * Whether reporting should be enabled for this test. - * Add a `GebConfig.groovy` to customize the reporter configuration. + * Add a {@code GebConfig.groovy} to customize the reporter configuration. */ - boolean reporting() default false + default boolean reporting() { + false + } /** - * The {@link org.openqa.selenium.remote.FileDetector} implementation to use for this class. - * <p> {@link NullContainerFileDetector} results in the - * {@link geb.testcontainers.serviceloader.ServiceRegistry last set} instance being used. + * The {@link ContainerFileDetector} implementation to use for this class. * * @since 4.2 - * @see DefaultContainerFileDetector DefaultContainerFileDetector - * @see UselessContainerFileDetector UselessContainerFileDetector + * @see DefaultContainerFileDetector + * @see UselessContainerFileDetector */ - Class<? extends ContainerFileDetector> fileDetector() default DefaultContainerFileDetector -} - -/** - * Inheritable version of {@link ContainerGebConfiguration}. - * - * @since 4.2 - */ -interface IContainerGebConfiguration { - - default String protocol() { - ContainerGebConfiguration.DEFAULT_PROTOCOL - } - - default String hostName() { - ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER - } - - default boolean reporting() { - false - } - default Class<? extends ContainerFileDetector> fileDetector() { - ContainerGebConfiguration.DEFAULT_FILE_DETECTOR + DefaultContainerFileDetector } } diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebExtension.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebExtension.groovy index aac4636f..a543f17e 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebExtension.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebExtension.groovy @@ -18,7 +18,6 @@ */ package geb.testcontainers -import geb.testcontainers.support.LocalhostDownloadSupport import groovy.transform.CompileStatic import groovy.transform.TailRecursive import groovy.util.logging.Slf4j @@ -35,8 +34,8 @@ import java.time.LocalDateTime * lifecycle for a {@link 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. + * {@link geb.test.ManagedGebTest ManagedGebTest} with a static test manager because + * the test manager needs to be rebuilt when the container is (re)initialized. * Instead, we initialize the same interceptors as the {@link geb.spock.GebExtension GebExtension} does. * * @author James Daugherty @@ -70,59 +69,54 @@ class ContainerGebExtension implements IGlobalExtension { @Override void visitSpec(SpecInfo spec) { - if (isContainerGebSpec(spec)) { - // Do not allow parallel execution since there's only 1 set of containers in testcontainers - spec.addExclusiveResource(exclusiveResource) - - // Always initialize the container requirements first so the GebTestManager can properly configure the browser - spec.addSharedInitializerInterceptor { invocation -> - holder.reinitialize(invocation) - - ContainerGebSpec gebSpec = invocation.sharedInstance as ContainerGebSpec - gebSpec.container = holder.container - gebSpec.testManager = holder.testManager - gebSpec.downloadSupport = new LocalhostDownloadSupport( - holder.browser, - holder.hostNameFromHost - ) - - // code below here is from the geb.spock.GebExtension since there can only be 1 shared initializer per extension - holder.testManager.beforeTestClass(invocation.spec.reflection) - invocation.proceed() - } + if (!isContainerGebSpec(spec)) { + return + } - spec.addSetupInterceptor { invocation -> - // Grails will be initialized by this point, so setup the browser url correctly - holder.setupBrowserUrl(invocation) - invocation.proceed() - } + // Do not allow parallel execution since there's only 1 set of containers in testcontainers + spec.addExclusiveResource(exclusiveResource) - spec.addInterceptor { invocation -> - try { - invocation.proceed() - } finally { - holder.testManager.afterTestClass() - } - } + // Always initialize the container requirements first so the GebTestManager can properly configure the browser + spec.addSharedInitializerInterceptor { invocation -> + holder.reinitialize(invocation) - spec.allFeatures*.addIterationInterceptor { invocation -> - holder.restartVncRecordingContainer() + ContainerGebSpec gebSpec = invocation.sharedInstance as ContainerGebSpec + gebSpec.holder = holder - holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName) - try { - invocation.proceed() - } finally { - holder.testManager.afterTest() - } - } + // Code below here is from the geb.spock.GebExtension since there can only be 1 shared initializer per extension + holder.testManager.beforeTestClass(invocation.spec.reflection) + invocation.proceed() + } - addGebExtensionOnFailureReporter(spec) + spec.addSetupInterceptor { invocation -> + // By this point, any server under test should be running, so setup the browser url correctly + holder.setupBrowserUrl(invocation) + invocation.proceed() + } - spec.addListener(new GebRecordingTestListener(holder)) + spec.addInterceptor { invocation -> + try { + invocation.proceed() + } finally { + holder.testManager.afterTestClass() + } + } + + spec.allFeatures*.addIterationInterceptor { invocation -> + holder.restartVncRecordingContainer() + holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName) + try { + invocation.proceed() + } finally { + holder.testManager.afterTest() + } } + + addOnFailureReporter(spec) + spec.addListener(new GebRecordingTestListener(holder)) } - private static void addGebExtensionOnFailureReporter(SpecInfo spec) { + private static void addOnFailureReporter(SpecInfo spec) { List<MethodInfo> methods = spec.allFeatures*.featureMethod + spec.allFixtureMethods.toList() methods.each { it.addInterceptor(new GebOnFailureReporter()) @@ -140,4 +134,3 @@ class ContainerGebExtension implements IGlobalExtension { return false } } - diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebSpec.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebSpec.groovy index 4b8ef27b..79e0759a 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebSpec.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/ContainerGebSpec.groovy @@ -18,20 +18,21 @@ */ package geb.testcontainers -import geb.Page +import geb.download.DownloadSupport +import geb.report.CompositeReporter +import geb.report.PageSourceReporter +import geb.report.Reporter +import geb.report.ScreenshotReporter import geb.test.GebTestManager -import geb.testcontainers.support.ContainerSupport -import geb.testcontainers.support.ReportingSupport -import geb.testcontainers.support.delegate.BrowserDelegate -import geb.testcontainers.support.delegate.DownloadSupportDelegate -import geb.testcontainers.support.delegate.DriverDelegate -import geb.testcontainers.support.delegate.PageDelegate -import groovy.transform.CompileStatic +import geb.transform.DynamicallyDispatchesToBrowser +import geb.testcontainers.support.ContainerGebFileInputSource +import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.images.builder.Transferable import spock.lang.Shared import spock.lang.Specification /** - * A {@link geb.spock.GebSpec GebSpec} that leverages Testcontainers + * A {@link geb.spock.GebSpec GebSpec} equivalent that leverages Testcontainers * to run the browser inside a container. * * <p>Prerequisites: @@ -49,19 +50,57 @@ import spock.lang.Specification * @author James Daugherty * @since 4.1 */ -@CompileStatic -abstract class ContainerGebSpec extends Specification implements ContainerSupport, ReportingSupport, BrowserDelegate, PageDelegate, DriverDelegate, DownloadSupportDelegate { +@DynamicallyDispatchesToBrowser +abstract class ContainerGebSpec extends Specification implements ContainerGebConfiguration { @Shared - static GebTestManager testManager + WebDriverContainerHolder holder - static void setTestManager(GebTestManager testManager) { - this.testManager = testManager + @Delegate(includes = ['getBrowser']) + GebTestManager getTestManager() { + holder?.testManager } - @Override - Page getPage() { - // Be explicit which trait to use (PageDelegate vs BrowserDelegate) - PageDelegate.super.page + /** + * Access the container running the web-driver, for convenience to execInContainer, copyFileToContainer etc. + */ + BrowserWebDriverContainer getContainer() { + holder?.container } -} \ No newline at end of file + + /** + * Returns the download support configured for localhost-aware downloads from containers. + */ + @Delegate(interfaces = false) + DownloadSupport getDownloadSupport() { + holder?.downloadSupport + } + + /** + * Reports the current state of the browser under the given label. + */ + void report(String message = '') { + testManager?.report(message) + } + + /** + * The reporter that Geb should use when reporting is enabled. + */ + Reporter createReporter() { + new CompositeReporter(new PageSourceReporter(), new ScreenshotReporter()) + } + + /** + * Copies a file from the host to the container for assignment to a Geb FileInput module. + * This method is useful when you need to upload a file to a form in a Geb test and will work cross-platform. + * + * @param hostPath relative path to the file on the host + * @param containerPath absolute path to where to put the file in the container + * @return the file object to assign to the FileInput module + * @since 4.2 + */ + File createFileInputSource(String hostPath, String containerPath) { + container.copyFileToContainer(Transferable.of(new File(hostPath).bytes), containerPath) + new ContainerGebFileInputSource(containerPath) + } +} diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/DefaultContainerFileDetector.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/DefaultContainerFileDetector.groovy index 040a8277..cccf4f2e 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/DefaultContainerFileDetector.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/DefaultContainerFileDetector.groovy @@ -36,11 +36,3 @@ class DefaultContainerFileDetector extends LocalFileDetector implements Containe */ class UselessContainerFileDetector extends UselessFileDetector implements ContainerFileDetector { } - -/** - * Used by {@link ContainerGebConfiguration#fileDetector()} interface to represent a null value. - * - * @since 4.2 - */ -class NullContainerFileDetector extends DefaultContainerFileDetector { -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebContainerSettings.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebContainerSettings.groovy index 795e683b..e753566d 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebContainerSettings.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebContainerSettings.groovy @@ -25,14 +25,14 @@ import groovy.util.logging.Slf4j import java.time.LocalDateTime import java.time.format.DateTimeFormatter -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 ContainerGebExtension}. + * Container-specific settings parsed from system properties. + * <p> + * Timeout and waiting configuration should be set in {@code GebConfig.groovy} + * using standard Geb mechanisms. * * @author James Daugherty * @since 4.1 @@ -41,51 +41,36 @@ import static org.testcontainers.containers.VncRecordingContainer.VncRecordingFo @CompileStatic class GebContainerSettings { - public static final boolean DEFAULT_AT_CHECK_WAITING = false private static final VncRecordingMode DEFAULT_RECORDING_MODE = VncRecordingMode.SKIP private static final VncRecordingFormat DEFAULT_RECORDING_FORMAT = VncRecordingFormat.MP4 - public static final int DEFAULT_TIMEOUT_IMPLICITLY_WAIT = 0 - public static final int DEFAULT_TIMEOUT_PAGE_LOAD = 300 - public static final int DEFAULT_TIMEOUT_SCRIPT = 30 boolean tracingEnabled String recordingDirectoryName String reportingDirectoryName - String browserType boolean restartRecordingContainerPerTest VncRecordingMode recordingMode VncRecordingFormat recordingFormat LocalDateTime startTime - int implicitlyWait - int pageLoadTimeout - int scriptTimeout - - boolean atCheckWaiting - Number timeout - Number retryInterval GebContainerSettings(LocalDateTime startTime) { - tracingEnabled = getBooleanProperty('geb.container.tracing.enabled', false) - recordingDirectoryName = System.getProperty('geb.container.recording.directory', 'build/gebContainer/recordings') - reportingDirectoryName = System.getProperty('geb.container.reporting.directory', 'build/gebContainer/reports') - // browserType = System.getProperty('geb.container.browser.type', DEFAULT_BROWSER_TYPE) - // browserType = System.getProperty('geb.env', DEFAULT_BROWSER_TYPE) + tracingEnabled = Boolean.parseBoolean( + System.getProperty('geb.container.tracing.enabled', 'false') + ) + recordingDirectoryName = System.getProperty( + 'geb.container.recording.directory', 'build/gebContainer/recordings' + ) + reportingDirectoryName = System.getProperty( + 'geb.container.reporting.directory', 'build/gebContainer/reports' + ) recordingMode = VncRecordingMode.valueOf( - System.getProperty('geb.container.recording.mode', DEFAULT_RECORDING_MODE.name()) + System.getProperty('geb.container.recording.mode', DEFAULT_RECORDING_MODE.name()) ) recordingFormat = VncRecordingFormat.valueOf( - System.getProperty('geb.container.recording.format', DEFAULT_RECORDING_FORMAT.name()) + System.getProperty('geb.container.recording.format', DEFAULT_RECORDING_FORMAT.name()) ) - restartRecordingContainerPerTest = getBooleanProperty( - 'geb.container.recording.restartRecordingContainerPerTest', - true + restartRecordingContainerPerTest = Boolean.parseBoolean( + System.getProperty('geb.container.recording.restartRecordingContainerPerTest', 'true') ) - implicitlyWait = getIntProperty('geb.container.timeouts.implicitlyWait', DEFAULT_TIMEOUT_IMPLICITLY_WAIT) - pageLoadTimeout = getIntProperty('geb.container.timeouts.pageLoad', DEFAULT_TIMEOUT_PAGE_LOAD) - scriptTimeout = getIntProperty('geb.container.timeouts.script', DEFAULT_TIMEOUT_SCRIPT) - atCheckWaiting = getBooleanProperty('geb.container.atCheckWaiting.enabled', DEFAULT_AT_CHECK_WAITING) - timeout = getNumberProperty('geb.container.timeouts.timeout', Wait.DEFAULT_TIMEOUT) - retryInterval = getNumberProperty('geb.container.timeouts.retryInterval', Wait.DEFAULT_RETRY_INTERVAL) this.startTime = startTime } @@ -109,34 +94,6 @@ class GebContainerSettings { createDirectory(reportingDirectoryName, 'reporting') } - private static boolean getBooleanProperty(String propertyName, boolean defaultValue) { - Boolean.parseBoolean(System.getProperty(propertyName, defaultValue.toString())) - } - - private static int getIntProperty(String propertyName, int defaultValue) { - System.getProperty(propertyName)?.toInteger() ?: defaultValue - } - - private static Number getNumberProperty(String propertyName, Number defaultValue) { - def propValue = System.getProperty(propertyName) - if (propValue) { - try { - if (propValue.contains('.')) { - return new BigDecimal(propValue) - } - 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 - } - private File createDirectory(String directoryName, String useCase) { def dir = new File( "$directoryName$File.separator${DateTimeFormatter.ofPattern('yyyyMMdd_HHmmss').format(startTime)}" @@ -151,7 +108,6 @@ class GebContainerSettings { "Configured $useCase directory [$dir] is expected to be a directory, but found file instead." ) } - return dir } } diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebOnFailureReporter.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebOnFailureReporter.groovy index fd278692..4900b620 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebOnFailureReporter.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebOnFailureReporter.groovy @@ -36,7 +36,7 @@ class GebOnFailureReporter implements IMethodInterceptor { throw notACauseForReporting } catch (Throwable throwable) { def spec = invocation.instance as ContainerGebSpec - if (spec.testManager.reportingEnabled) { + if (spec.testManager?.reportingEnabled) { try { spec.testManager.reportFailure() } catch (ignored) {} diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebRecordingTestListener.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebRecordingTestListener.groovy index 1ba98ddf..dfe2b1c7 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebRecordingTestListener.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/GebRecordingTestListener.groovy @@ -74,4 +74,4 @@ class GebRecordingTestListener extends AbstractRunListener { void error(ErrorInfo error) { errorInfo = error } -} \ No newline at end of file +} diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/WebDriverContainerHolder.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/WebDriverContainerHolder.groovy index fab7f18e..34335c06 100644 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/WebDriverContainerHolder.groovy +++ b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/WebDriverContainerHolder.groovy @@ -18,30 +18,25 @@ */ package geb.testcontainers -import org.openqa.selenium.firefox.FirefoxOptions - -import java.time.Duration -import java.time.temporal.ChronoUnit -import java.util.function.Supplier - -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 import geb.Configuration import geb.ConfigurationLoader +import geb.download.DownloadSupport import geb.spock.SpockGebTestManagerBuilder import geb.test.GebTestManager -import geb.waiting.Wait +import geb.testcontainers.support.LocalhostDownloadSupport +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.PackageScope +import groovy.util.logging.Slf4j import org.openqa.selenium.SessionNotCreatedException +import org.openqa.selenium.firefox.FirefoxOptions 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.GenericContainer import org.testcontainers.containers.BrowserWebDriverContainer import org.testcontainers.containers.ContainerFetchException import org.testcontainers.containers.PortForwardingContainer @@ -50,15 +45,12 @@ import org.testcontainers.containers.VncRecordingContainer import org.testcontainers.images.PullPolicy import org.testcontainers.utility.DockerImageName -import geb.testcontainers.serviceloader.ServiceRegistry - -import static GebContainerSettings.DEFAULT_AT_CHECK_WAITING -import static GebContainerSettings.DEFAULT_TIMEOUT_IMPLICITLY_WAIT -import static GebContainerSettings.DEFAULT_TIMEOUT_PAGE_LOAD -import static GebContainerSettings.DEFAULT_TIMEOUT_SCRIPT +import java.time.Duration +import java.time.temporal.ChronoUnit +import java.util.function.Supplier /** - * Responsible for initializing a {@link org.testcontainers.containers.BrowserWebDriverContainer} + * Responsible for initializing a {@link BrowserWebDriverContainer} * per the Spec's {@link ContainerGebConfiguration}. This class will try to * reuse the same container if the configuration matches the current container. * @@ -69,10 +61,6 @@ import static GebContainerSettings.DEFAULT_TIMEOUT_SCRIPT @CompileStatic class WebDriverContainerHolder { - /* Driver defaults to Firefox due to availability on AMD64 and ARM64 platforms. - * {@link https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#experimental-multi-arch-amd64aarch64armhf-images Selenium Browser Archetectures} - */ - private static final String DEFAULT_HOSTNAME_FROM_HOST = 'localhost' private static final String REMOTE_ADDRESS_PROPERTY = 'webdriver.remote.server' private static final String HOST_PORT_PROPERTY = 'hostPort' @@ -85,7 +73,8 @@ class WebDriverContainerHolder { GebTestManager testManager Browser browser BrowserWebDriverContainer container - WebDriverContainerConfiguration containerConf + DownloadSupport downloadSupport + SpecContainerConfiguration containerConf WebDriverContainerHolder(GebContainerSettings settings) { this.settings = settings @@ -96,23 +85,24 @@ class WebDriverContainerHolder { } void stop() { + System.clearProperty(REMOTE_ADDRESS_PROPERTY) container?.stop() container = null browser = null testManager = null + downloadSupport = null containerConf = null } - boolean matchesCurrentContainerConfiguration(WebDriverContainerConfiguration specConf) { + boolean matchesCurrentContainerConfiguration(SpecContainerConfiguration specConf) { specConf == containerConf && settings.recordingMode == BrowserWebDriverContainer.VncRecordingMode.SKIP } @PackageScope boolean reinitialize(IMethodInvocation methodInvocation) { - def specConf = new WebDriverContainerConfiguration( - methodInvocation.spec - ) + def specConf = new SpecContainerConfiguration(methodInvocation.spec) + if (matchesCurrentContainerConfiguration(specConf)) { return false } @@ -127,15 +117,14 @@ class WebDriverContainerHolder { def customBrowser = gebConf.rawConfig.containerBrowser as String if (gebConfigExists) { - log.info("A Geb configuration exists...") + log.info('A Geb configuration exists...') validateDriverConf(gebConf) + if (customBrowser) { log.info( 'A \'containerBrowser\' property was found in GebConfig. ' + "Using [$customBrowser] container image." ) - // Prepare for creating a container matching - // the GebConfig `containerBrowser` property. dockerImageName = createDockerImageName(customBrowser) } else { log.info( @@ -154,16 +143,12 @@ class WebDriverContainerHolder { container.with { withEnv('SE_ENABLE_TRACING', settings.tracingEnabled.toString()) - // Disable withAccessToHost when running in a container (CI environment) - // as SSH port forwarding doesn't work well in container-in-container setups if (!System.getenv(CI_PROPERTY)) { withAccessToHost(true) } else { - // Increase startup timeout for CI environments (container-in-container is slower) withStartupTimeout(Duration.of(2, ChronoUnit.MINUTES)) } withImagePullPolicy(PullPolicy.ageBased(Duration.of(1, ChronoUnit.DAYS))) - // start() // without Capabilities this is starting chrome } startContainer(container, dockerImageName, customBrowser) @@ -176,29 +161,21 @@ class WebDriverContainerHolder { } // 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 - - // As we don't cache, this will have been defaulted to true. We override to false. gebConf.quitDriverOnBrowserReset = false - gebConf.baseUrl = container.seleniumAddress + if (containerConf.reporting) { gebConf.reportsDir = settings.reportingDirectory gebConf.reporter = (methodInvocation.sharedInstance as ContainerGebSpec).createReporter() } - 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 - ) - } else { - // If no driver was set in GebConfig, create a Firefox driver + // Set the selenium address as a system property so that RemoteWebDriver + // constructors (without explicit URL) can find it. Tests run serially + // (enforced by ExclusiveResource) so this is safe. + System.setProperty(REMOTE_ADDRESS_PROPERTY, container.seleniumAddress.toString()) + + if (!gebConf.driverConf) { gebConf.driverConf = { -> log.info('Using default Firefox RemoteWebDriver for {}', container.seleniumAddress) new RemoteWebDriver(container.seleniumAddress, new FirefoxOptions()) @@ -207,17 +184,15 @@ class WebDriverContainerHolder { browser = createBrowser(gebConf) applyFileDetector(browser, containerConf) - applyTimeouts(browser, settings) // 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). + // when the shared fields are initialized, which is before the server under test has started, + // so the real url cannot be set yet. 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' + downloadSupport = new LocalhostDownloadSupport(browser, hostNameFromHost) testManager = createTestManager() - return true } @@ -226,9 +201,8 @@ class WebDriverContainerHolder { return } - // use a configured baseUrl if found, otherwise use localhost and hostPort def baseUrl = findBaseUrl(methodInvocation) - if (baseUrl != "") { + if (baseUrl) { browser.baseUrl = baseUrl } else { int hostPort = findServerPort(methodInvocation) @@ -243,10 +217,8 @@ class WebDriverContainerHolder { * 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 + * is different from the default, this method will return the same value as * {@code webDriverContainer.getHost()}. - * - * @return the hostname for accessing the server under test from the host */ String getHostNameFromHost() { hostNameChanged ? container.host : DEFAULT_HOSTNAME_FROM_HOST @@ -256,60 +228,46 @@ class WebDriverContainerHolder { * Workaround for https://github.com/testcontainers/testcontainers-java/issues/3998 * <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. + * test method. Uses reflection to access the VNC recording container field in + * BrowserWebDriverContainer. Should be called BEFORE each test starts. */ void restartVncRecordingContainer() { if (!settings.recordingEnabled || !settings.restartRecordingContainerPerTest || !container) { return } + try { - // Use reflection to access the VNC recording container field 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 def newVncContainer = new VncRecordingContainer(container) .withVncPassword('secret') .withVncPort(5900) .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) - // Don't throw the exception to avoid breaking the test execution } } - /** - * Returns a host port to use for the container in this order: - * Class field 'hostPort' if found in the invocation (Spec). - * A 'hostPort' property in the GebConfig configuration. - * @param methodInvocation The method invocation. - * @return int The server port. - */ private static int findServerPort(IMethodInvocation methodInvocation) { - // use the test class hostPort if specified + // Use getMetaProperty to check for a real declared property, not propertyMissing + // (which @DynamicallyDispatchesToBrowser would intercept) try { - return (int) methodInvocation.instance.metaClass.getProperty( - methodInvocation.instance, - HOST_PORT_PROPERTY - ) + if (methodInvocation.instance.metaClass.getMetaProperty(HOST_PORT_PROPERTY)) { + return (int) methodInvocation.instance[HOST_PORT_PROPERTY] + } } catch (ignored) { - // Grails throws an IllegalStateException about their annotation here. We do not. log.info("no ${HOST_PORT_PROPERTY} from methodInvocation") } try { - // use geb config setting or default 8080 Configuration gebConfig = new ConfigurationLoader().conf return (int) gebConfig.rawConfig.getProperty(HOST_PORT_PROPERTY) } catch (ignored) { @@ -320,34 +278,25 @@ class WebDriverContainerHolder { return 8080 } - /** - * Returns a baseUrl from a setting if found in this order: - * A class field 'baseUrl' if found in the invocation (Spec). - * A 'baseUrl' property in the GebConfig configuration. - * Or an empty String if not found. - * @param methodInvocation The method invocation. - * @return String The base URL. - */ private static String findBaseUrl(IMethodInvocation methodInvocation) { - // use the test class baseUrl if specified + // Use getMetaProperty to check for a real declared property, not propertyMissing + // (which @DynamicallyDispatchesToBrowser would intercept) try { - String baseUrl = methodInvocation.instance.metaClass.getProperty( - methodInvocation.instance, - BASE_URL_PROPERTY - ) - if (baseUrl) { - log.info("using ${BASE_URL_PROPERTY}: ${baseUrl} from method invocation.") - return baseUrl + if (methodInvocation.instance.metaClass.getMetaProperty(BASE_URL_PROPERTY)) { + String baseUrl = methodInvocation.instance[BASE_URL_PROPERTY] + if (baseUrl) { + log.info("using ${BASE_URL_PROPERTY}: ${baseUrl} from method invocation.") + return baseUrl + } } } catch (ignored) { log.info("no ${BASE_URL_PROPERTY} from methodInvocation") } try { - // use geb config setting or default Configuration gebConfig = new ConfigurationLoader().conf - String baseUrl = gebConfig.getProperty(BASE_URL_PROPERTY) ?: "" - if (baseUrl != "") { + String baseUrl = gebConfig.getProperty(BASE_URL_PROPERTY) ?: '' + if (baseUrl) { log.info("using ${BASE_URL_PROPERTY}: ${baseUrl} from configuration.") return baseUrl } @@ -356,7 +305,7 @@ class WebDriverContainerHolder { } log.info("no configured ${BASE_URL_PROPERTY} found.") - return "" + return '' } private static Browser createBrowser(Configuration gebConf) { @@ -375,37 +324,8 @@ class WebDriverContainerHolder { browser } - 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 - ) - } - - private static void applyTimeouts(Browser browser, GebContainerSettings settings) { - // Overwrite `GebConfig` timeouts with values explicitly set in - // `GebContainerSettings` (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 - } + private static void applyFileDetector(Browser browser, SpecContainerConfiguration conf) { + ((RemoteWebDriver) browser.driver).fileDetector = conf.fileDetector.getDeclaredConstructor().newInstance() } private static void startContainer(BrowserWebDriverContainer container, DockerImageName dockerImageName, String customBrowser) { @@ -426,12 +346,8 @@ class WebDriverContainerHolder { } private static String getHostIp() { - // In CI environments (container-in-container), - // withAccessToHost is disabled and PortForwardingContainer is not used. - // Get the actual IP address of this container so other containers can reach it. if (System.getenv(CI_PROPERTY)) { try { - // Execute hostname -I to get the container's IP address def process = 'hostname -I'.execute() process.waitFor() def ip = process.text.trim().split(/\s+/)[0] @@ -441,7 +357,6 @@ class WebDriverContainerHolder { } catch (Exception e) { log.warn('Failed to get container IP via hostname -I', e) } - // Fallback to localhost return LOCALHOST_IP } @@ -494,103 +409,38 @@ class WebDriverContainerHolder { } private boolean isHostnameChanged() { - containerConf.hostName != ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER + containerConf.hostName != SpecContainerConfiguration.DEFAULT_HOSTNAME } private boolean isHostNameChanged() { - container.host != ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER + container.host != SpecContainerConfiguration.DEFAULT_HOSTNAME } @CompileStatic @EqualsAndHashCode - private static class WebDriverContainerConfiguration { + @PackageScope + static class SpecContainerConfiguration { + + static final String DEFAULT_PROTOCOL = 'http' + static final String DEFAULT_HOSTNAME = GenericContainer.INTERNAL_HOST_HOSTNAME + static final Class<? extends ContainerFileDetector> DEFAULT_FILE_DETECTOR = DefaultContainerFileDetector String protocol String hostName boolean reporting Class<? extends ContainerFileDetector> fileDetector - WebDriverContainerConfiguration(SpecInfo spec) { - ContainerGebConfiguration conf + SpecContainerConfiguration(SpecInfo spec) { + ContainerGebConfiguration conf = null - // Check if the class implements the interface - if (IContainerGebConfiguration.isAssignableFrom(spec.reflection)) { + if (ContainerGebConfiguration.isAssignableFrom(spec.reflection)) { conf = spec.reflection.getConstructor().newInstance() as ContainerGebConfiguration - } else { - // Check for the annotation - conf = spec.annotations.find { - it.annotationType() == ContainerGebConfiguration - } as ContainerGebConfiguration } - protocol = conf?.protocol() ?: ContainerGebConfiguration.DEFAULT_PROTOCOL - hostName = conf?.hostName() ?: ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER + protocol = conf?.protocol() ?: DEFAULT_PROTOCOL + hostName = conf?.hostName() ?: DEFAULT_HOSTNAME reporting = conf?.reporting() ?: false - fileDetector = conf?.fileDetector() ?: ContainerGebConfiguration.DEFAULT_FILE_DETECTOR + fileDetector = conf?.fileDetector() ?: DEFAULT_FILE_DETECTOR } } - - @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 ThreadLocal<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/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/serviceloader/ServiceRegistry.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/serviceloader/ServiceRegistry.groovy deleted file mode 100644 index 6abb58ab..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/serviceloader/ServiceRegistry.groovy +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.serviceloader - -import groovy.transform.CompileStatic - -/** - * A service registry that loads and caches different service types using the Java - * {@link java.util.ServiceLoader}, while allowing overriding which instance to return. - * <p> - * This class provides thread-safe service loading and caching, supporting parallel test execution. - * It provides both automatic service discovery through {@link java.util.ServiceLoader} - * and explicit replacement of the returned service instance for customization. - * </p> - * <p> - * Usage example: - * <pre><code> - * MyService service = ServiceRegistry.getInstance(MyService, DefaultMyService) - * </code></pre> - * </p> - * - * @since 4.2 - * @author Mattias Reichel - */ -@CompileStatic -class ServiceRegistry { - - private static final ThreadLocal<HashMap<Class<?>, Object>> INSTANCES = ThreadLocal.withInitial { - [:] - } as ThreadLocal<HashMap<Class<?>, Object>> - - /** - * Returns the service instance of the given service type, loading it using - * {@link java.util.ServiceLoader} if not already loaded or an instance - * of the default implementation type if no service implementation is found - * by the {@link java.util.ServiceLoader}. - * - * If an instance has been set using {@link #setInstance(Class, Object)} or - * {@link #setInstance(Class, Class)}, that instance will be returned instead. - * - * @param serviceType The service type - * @param defaultType The service implementation type to use if no service - * implementation is found (Must have a zero-argument constructor) - * @return An instance of the service type - */ - static <T> T getInstance(Class<T> serviceType, Class<? extends T> defaultType) { - (T) INSTANCES.get().computeIfAbsent(serviceType) { - ServiceLoader.load(serviceType) - .findFirst() - .orElseGet { defaultType.getDeclaredConstructor().newInstance() } - } - } - - /** - * Sets the instance to be returned for the given service type, bypassing instance - * loading from {@link java.util.ServiceLoader}. - * <p> - * Setting the instance to {@code null} will revert to loading the the service instance - * via the {@link java.util.ServiceLoader}. - * </p> - * @param serviceType The service type for which the instance should be set - * @param instance The instance to return for the given service type, or - * {@code null} for default service loading - */ - static <T> void setInstance(Class<T> serviceType, T instance) { - INSTANCES.get().put(serviceType, instance) - } - - /** - * Sets the implementation type to return for the given service type, bypassing instance loading - * from {@link java.util.ServiceLoader}. - * - * @param serviceType The service type for which the instance should be set - * @param instanceType The type of the instance to return for the given service type. - * (Must have a zero-argument constructor). - */ - static <T> void setInstance(Class<T> serviceType, Class<? extends T> instanceType) { - setInstance(serviceType, instanceType.getDeclaredConstructor().newInstance()) - } -} diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/BrowserType.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/BrowserType.groovy deleted file mode 100644 index 4ab4de73..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/BrowserType.groovy +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support - -/** - * Represents the supported browser types. - * - * <p>This enum defines the available browser options that can be used.</p> - */ -enum BrowserType { - - /** - * Google Chrome browser. - */ - - CHROME, - /** - * Mozilla Firefox browser. - */ - FIREFOX, - - /** - * Microsoft Edge browser. - */ - EDGE -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ContainerSupport.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ContainerSupport.groovy deleted file mode 100644 index 82d53194..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ContainerSupport.groovy +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support - -import geb.download.DownloadSupport -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileStatic -import groovy.transform.SelfType -import org.testcontainers.containers.BrowserWebDriverContainer -import org.testcontainers.images.builder.Transferable -import spock.lang.Shared - -/** - * Features for supporting Geb tests running in a container. - * - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait ContainerSupport implements DownloadSupport { - - /** - * Get access to container running the web-driver, for convenience to execInContainer, copyFileToContainer etc. - * - * @see org.testcontainers.containers.ContainerState#execInContainer(java.lang.String ...) - * @see org.testcontainers.containers.ContainerState#copyFileToContainer(org.testcontainers.utility.MountableFile, java.lang.String) - * @see org.testcontainers.containers.ContainerState#copyFileFromContainer(java.lang.String, java.lang.String) - * @see org.testcontainers.containers.ContainerState - */ - @Shared - static BrowserWebDriverContainer container - - static void setContainer(BrowserWebDriverContainer container) { - this.container = container - } - - @Shared - static DownloadSupport downloadSupport - - /** - * Sets the {@link DownloadSupport} instance to use for file downloads. - * This allows for setting a custom implementation of {@code DownloadSupport} - * when downloading from a container. - * - * @param downloadSupport the {@code DownloadSupport} instance to use - * @since 4.1 - */ - static void setDownloadSupport(DownloadSupport downloadSupport) { - this.downloadSupport = downloadSupport - } - - /** - * Copies a file from the host to the container for assignment to a Geb FileInput module. - * This method is useful when you need to upload a file to a form in a Geb test and will work cross-platform. - * - * @param hostPath relative path to the file on the host - * @param containerPath absolute path to where to put the file in the container - * @return the file object to assign to the FileInput module - * @since 4.2 - */ - File createFileInputSource(String hostPath, String containerPath) { - container.copyFileToContainer(Transferable.of(new File(hostPath).bytes), containerPath) - return new ContainerGebFileInputSource(containerPath) - } -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ReportingSupport.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ReportingSupport.groovy deleted file mode 100644 index 98267bff..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/ReportingSupport.groovy +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support - -import geb.report.CompositeReporter -import geb.report.PageSourceReporter -import geb.report.Reporter -import geb.report.ScreenshotReporter -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileStatic -import groovy.transform.SelfType - -/** - * Enables reporting support for ContainerGebSpec. - * - * @author James Daugherty - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait ReportingSupport { - - void report(String message) { - ContainerGebSpec.testManager.report(message) - } - - /** - * The reporter that Geb should use when reporting is enabled. - */ - Reporter createReporter() { - return new CompositeReporter(new PageSourceReporter(), new ScreenshotReporter()) - } -} diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/BrowserDelegate.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/BrowserDelegate.groovy deleted file mode 100644 index db4893e0..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/BrowserDelegate.groovy +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support.delegate - -import geb.Browser -import geb.Page -import geb.js.JavascriptInterface -import geb.url.UrlFragment -import geb.webstorage.WebStorage -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileDynamic -import groovy.transform.CompileStatic -import groovy.transform.SelfType -import org.openqa.selenium.WebDriver - -import java.time.Duration - -import static groovy.lang.Closure.DELEGATE_FIRST - -/** - * Handles delegation to the browser instance so that the Geb API can be used directly in the test. - * <p> - * As method parameter names are not available in the Geb artifacts we are delegating manually, - * instead of using @Delegate AST transform, to get the best possible end user IDE support and user experience. - * - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait BrowserDelegate { - - /** - * Accessor to the Geb {@link geb.Browser} instance. - */ - Browser getBrowser() { - ContainerGebSpec.testManager.browser - } - - /** - * Delegates to {@link geb.Browser#getDriver()}. - */ - WebDriver getDriver() { - browser.driver - } - - /** - * Delegates to {@link geb.Browser#getCurrentUrl()}. - */ - String getCurrentUrl() { - browser.currentUrl - } - - /** - * Delegates to {@link geb.Browser#getPage()}. - */ - Page getPage() { - browser.page - } - - /** - * Delegates to {@link geb.Browser#at(Class)}. - */ - <T extends Page> T at(Class<T> pageType) { - browser.at(pageType) - } - - /** - * Delegates to {@link geb.Browser#at(Class, Closure)}. - */ - <T extends Page, R> R at(Class<T> pageType, @DelegatesTo(strategy = DELEGATE_FIRST, genericTypeIndex = 0) Closure<R> assertions) { - browser.at(at(pageType), assertions) - } - - /** - * Delegates to {@link geb.Browser#at(Page)}. - */ - <T extends Page> T at(T page) { - browser.at(page) - } - - /** - * Delegates to {@link geb.Browser#isAt(Class, boolean)}. - */ - boolean isAt(Class<? extends Page> pageType, boolean honourGlobalAtCheckWaiting = true) { - browser.isAt(pageType, honourGlobalAtCheckWaiting) - } - - /** - * Delegates to {@link geb.Browser#isAt(Page, boolean)}. - */ - boolean isAt(Page page, boolean honourGlobalAtCheckWaiting = true) { - browser.isAt(page, honourGlobalAtCheckWaiting) - } - - /** - * Delegates to {@link geb.Browser#checkIfAtAnUnexpectedPage(Class[])}. - */ - void checkIfAtAnUnexpectedPage(Class<? extends Page>[] expectedPages) { - browser.checkIfAtAnUnexpectedPage(expectedPages) - } - - /** - * Delegates to {@link geb.Browser#checkIfAtAnUnexpectedPage(Page[])}. - */ - void checkIfAtAnUnexpectedPage(Page[] expectedPages) { - browser.checkIfAtAnUnexpectedPage(expectedPages) - } - - /** - * Delegates to {@link geb.Browser#go(String)}. - */ - void go(String url) { - browser.go(url) - } - - /** - * Delegates to {@link geb.Browser#go(String, UrlFragment)}. - */ - void go(String url, UrlFragment fragment) { - browser.go(url, fragment) - } - - /** - * Delegates to {@link geb.Browser#go(Map, UrlFragment)}. - */ - void go(Map params = [:], UrlFragment fragment) { - browser.go(params, fragment) - } - - /** - * Delegates to {@link geb.Browser#go(Map, String, UrlFragment)}. - */ - void go(Map params = [:], String url = null, UrlFragment fragment = null) { - browser.go(params, url, fragment) - } - - /** - * Delegates to {@link geb.Browser#to(Map, Class, Object[])}. - */ - <T extends Page> T to(Map params = [:], Class<T> pageType, Object[] args) { - browser.to(params, pageType, args) - } - - /** - * Delegates to {@link geb.Browser#to(Map, Class, UrlFragment, Object[])}. - */ - <T extends Page> T to(Map params = [:], Class<T> pageType, UrlFragment fragment, Object[] args) { - browser.to(params, pageType, fragment, args) - } - - /** - * Delegates to {@link geb.Browser#to(Map, Page, Object[])}. - */ - <T extends Page> T to(Map params = [:], T page, Object[] args) { - browser.to(params, page, args) - } - - /** - * Delegates to {@link geb.Browser#to(Map, Page, UrlFragment, Object[])}. - */ - <T extends Page> T to(Map params = [:], T page, UrlFragment fragment, Object[] args) { - browser.to(params, page, fragment, args) - } - - /** - * Delegates to {@link geb.Browser#via(Map, Class, Object[])}. - */ - <T extends Page> T via(Map params = [:], Class<T> pageType, Object[] args) { - browser.via(params, pageType, args) - } - - /** - * Delegates to {@link geb.Browser#via(Map, Class, UrlFragment, Object[])}. - */ - <T extends Page> T via(Map params = [:], Class<T> pageType, UrlFragment fragment, Object[] args) { - browser.via(params, pageType, fragment, args) - } - - /** - * Delegates to {@link geb.Browser#via(Map, Page, Object[])}. - */ - <T extends Page> T via(Map params = [:], T page, Object[] args) { - browser.via(params, page, args) - } - - /** - * Delegates to {@link geb.Browser#via(Map, Page, UrlFragment, Object[])}. - */ - <T extends Page> T via(Map params = [:], T page, UrlFragment fragment, Object[] args) { - browser.via(params, page, fragment, args) - } - - /** - * Delegates to {@link geb.Browser#clearCookies(String[])}. - */ - void clearCookies(String... additionalUrls) { - browser.clearCookies(additionalUrls) - } - - /** - * Delegates to {@link geb.Browser#clearCookies()}. - */ - void clearCookies() { - browser.clearCookies() - } - - /** - * Delegates to {@link geb.Browser#clearCookiesQuietly()}. - */ - void clearCookiesQuietly() { - browser.clearCookiesQuietly() - } - - /** - * Delegates to {@link geb.Browser#clearWebStorage()}. - */ - void clearWebStorage() { - browser.clearWebStorage() - } - - /** - * Delegates to {@link geb.Browser#clearWebStorageQuietly()}. - */ - void clearWebStorageQuietly() { - browser.clearWebStorageQuietly() - } - - /** - * Delegates to {@link geb.Browser#quit()}. - */ - void quit() { - browser.quit() - } - - /** - * Delegates to {@link geb.Browser#close()}. - */ - void close() { - browser.close() - } - - /** - * Delegates to {@link geb.Browser#getAvailableWindows()}. - */ - Set<String> getAvailableWindows() { - browser.getAvailableWindows() - } - - /** - * Delegates to {@link geb.Browser#withWindow(String, Closure)}. - */ - <T> T withWindow(String window, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withWindow(window, block) - } - - /** - * Delegates to {@link geb.Browser#withWindow(Closure, Closure)}. - */ - <T> List<T> withWindow(Closure specification, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withWindow(specification, block) - } - - /** - * Delegates to {@link geb.Browser#withWindow(Map, Closure, Closure)}. - */ - <T> List<T> withWindow(Map options, Closure specification, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withWindow(options, specification, block) - } - - /** - * Delegates to {@link geb.Browser#withWindow(Map, String, Closure)}. - */ - <T> T withWindow(Map options, String window, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withWindow(options, window, block) - } - - /** - * Delegates to {@link geb.Browser#withWindow(Map, Closure, Closure)}. - */ - <T> T withNewWindow(Map options, Closure windowOpeningBlock, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withNewWindow(options, windowOpeningBlock, block) - } - - /** - * Delegates to {@link geb.Browser#withNewWindow(Closure, Closure)}. - */ - <T> T withNewWindow(Closure windowOpeningBlock, @DelegatesTo(value = Browser, strategy = DELEGATE_FIRST) Closure<T> block) { - browser.withNewWindow(windowOpeningBlock, block) - } - - /** - * Delegates to {@link geb.Browser#createPage(Class)}. - */ - <T extends Page> T createPage(Class<T> pageType) { - browser.createPage(pageType) - } - - /** - * Delegates to {@link geb.Browser#getJs()}. - */ - JavascriptInterface getJs() { - browser.js - } - - /** - * Delegates to {@link geb.Browser#getReportGroupDir()}. - */ - File getReportGroupDir() { - browser.reportGroupDir - } - - /** - * Delegates to {@link geb.Browser#reportGroup(String)}. - */ - void reportGroup(String path) { - browser.reportGroup(path) - } - - /** - * Delegates to {@link geb.Browser#reportGroup(Class)}. - */ - void reportGroup(Class clazz) { - browser.reportGroup(clazz) - } - - /** - * Delegates to {@link geb.Browser#cleanReportGroupDir()}. - */ - void cleanReportGroupDir() { - browser.cleanReportGroupDir() - } - - /** - * Delegates to {@link geb.Browser#pause()}. - */ - void pause() { - browser.pause() - } - - /** - * Delegates to {@link geb.Browser#getLocalStorage()}. - */ - WebStorage getLocalStorage() { - browser.localStorage - } - - /** - * Delegates to {@link geb.Browser#getSessionStorage()}. - */ - WebStorage getSessionStorage() { - browser.sessionStorage - } - - /** - * Delegates to {@link geb.Browser#verifyAtImplicitly(Class)}. - */ - void verifyAtImplicitly(Class<? extends Page> targetPage) { - browser.verifyAtImplicitly(targetPage) - } - - /** - * Delegates to {@link geb.Browser#verifyAtImplicitly(Page)}. - */ - void verifyAtImplicitly(Page targetPage) { - browser.verifyAtImplicitly(targetPage) - } - - /** - * Delegates to {@link geb.Browser#setNetworkLatency(Duration)}. - */ - void setNetworkLatency(Duration duration) { - browser.networkLatency = duration - } - - /** - * Delegates to {@link geb.Browser#resetNetworkLatency()}. - */ - void resetNetworkLatency() { - browser.resetNetworkLatency() - } - - /** - * Delegates to {@link geb.Browser#driverAs(Class)}. - */ - <T> Optional<T> driverAs(Class<T> castType) { - browser.driverAs(castType) - } - - /** - * Delegates missing method calls to the current {@link geb.Page} instance. - */ - @CompileDynamic - def methodMissing(String name, args) { - browser.page."$name"(*args) - } - - /** - * Delegates missing property accesses to the current {@link geb.Page} instance. - */ - @CompileDynamic - def propertyMissing(String name) { - browser.page."$name" - } - - /** - * Delegates missing property mutations to the current {@link geb.Page} instance. - */ - @CompileDynamic - def propertyMissing(String name, value) { - browser.page."$name" = value - } -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DownloadSupportDelegate.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DownloadSupportDelegate.groovy deleted file mode 100644 index d7740377..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DownloadSupportDelegate.groovy +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support.delegate - -import geb.download.DownloadSupport -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileStatic -import groovy.transform.SelfType - -/** - * Handles delegation to the DownloadSupport instance so that the Geb API can be used directly in the test. - * <p> - * As method parameter names are not available in the Geb artifacts we are delegating manually to - * get the best possible IDE support and user experience. - * - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait DownloadSupportDelegate implements DownloadSupport { - - @Override - HttpURLConnection download() { - ContainerGebSpec.downloadSupport.download() - } - - @Override - HttpURLConnection download(Map options) { - ContainerGebSpec.downloadSupport.download(options) - } - - @Override - HttpURLConnection download(String uri) { - ContainerGebSpec.downloadSupport.download(uri) - } - - @Override - InputStream downloadStream() { - ContainerGebSpec.downloadSupport.downloadStream() - } - - @Override - InputStream downloadStream(Map options) { - ContainerGebSpec.downloadSupport.downloadStream(options) - } - - @Override - InputStream downloadStream(Map options, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadStream(options, connectionConfig) - } - - @Override - InputStream downloadStream(String uri) { - ContainerGebSpec.downloadSupport.downloadStream(uri) - } - - @Override - InputStream downloadStream(String uri, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadStream(uri, connectionConfig) - } - - @Override - InputStream downloadStream(Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadStream(connectionConfig) - } - - @Override - String downloadText() { - ContainerGebSpec.downloadSupport.downloadText() - } - - @Override - String downloadText(Map options) { - ContainerGebSpec.downloadSupport.downloadText(options) - } - - @Override - String downloadText(Map options, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadText(options, connectionConfig) - } - - @Override - String downloadText(String uri) { - ContainerGebSpec.downloadSupport.downloadText(uri) - } - - @Override - String downloadText(String uri, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadText(uri, connectionConfig) - } - - @Override - String downloadText(Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadText(connectionConfig) - } - - @Override - byte[] downloadBytes() { - ContainerGebSpec.downloadSupport.downloadBytes() - } - - @Override - byte[] downloadBytes(Map options) { - ContainerGebSpec.downloadSupport.downloadBytes(options) - } - - @Override - byte[] downloadBytes(Map options, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadBytes(options, connectionConfig) - } - - @Override - byte[] downloadBytes(Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadBytes(connectionConfig) - } - - @Override - byte[] downloadBytes(String uri) { - ContainerGebSpec.downloadSupport.downloadBytes(uri) - } - - @Override - byte[] downloadBytes(String uri, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadBytes(uri, connectionConfig) - } - - @Override - Object downloadContent() { - ContainerGebSpec.downloadSupport.downloadContent() - } - - @Override - Object downloadContent(Map options) { - ContainerGebSpec.downloadSupport.downloadContent(options) - } - - @Override - Object downloadContent(Map options, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadContent(options, connectionConfig) - } - - @Override - Object downloadContent(String uri) { - ContainerGebSpec.downloadSupport.downloadContent(uri) - } - - @Override - Object downloadContent(String uri, Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadContent(uri, connectionConfig) - } - - @Override - Object downloadContent(Closure connectionConfig) { - ContainerGebSpec.downloadSupport.downloadContent(connectionConfig) - } -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DriverDelegate.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DriverDelegate.groovy deleted file mode 100644 index 05af54c0..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/DriverDelegate.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support.delegate - -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileStatic -import groovy.transform.SelfType - -/** - * Handles delegation to select methods of the driver instance for end user convenience. - * - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait DriverDelegate { - - /** - * Get the source of the last loaded page. If the page has been modified after loading (for - * example, by Javascript) there is no guarantee that the returned text is that of the modified - * page. Please consult the documentation of the particular driver being used to determine whether - * the returned text reflects the current state of the page or the text last sent by the web - * server. The page source returned is a representation of the underlying DOM: do not expect it to - * be formatted or escaped in the same way as the response sent from the web server. Think of it - * as an artist's impression. - * - * <p>See <a href="https://w3c.github.io/webdriver/#get-page-source">W3C WebDriver - * specification</a> for more details. - * - * @return The source of the current page - */ - String getPageSource() { - ContainerGebSpec.testManager.browser.driver.pageSource - } - -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/PageDelegate.groovy b/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/PageDelegate.groovy deleted file mode 100644 index bafba5cf..00000000 --- a/integration/geb-testcontainers/src/main/groovy/geb/testcontainers/support/delegate/PageDelegate.groovy +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package geb.testcontainers.support.delegate - -import geb.Module -import geb.Page -import geb.content.Navigable -import geb.content.TemplateDerivedPageContent -import geb.frame.FrameSupport -import geb.interaction.InteractDelegate -import geb.interaction.InteractionsSupport -import geb.js.AlertAndConfirmSupport -import geb.navigator.Navigator -import geb.textmatching.TextMatchingSupport -import geb.url.UrlFragment -import geb.waiting.WaitingSupport -import geb.testcontainers.ContainerGebSpec -import groovy.transform.CompileStatic -import groovy.transform.SelfType -import org.openqa.selenium.By -import org.openqa.selenium.WebElement - -/** - * Handles delegation to the page instance so that the Geb API can be used directly in the test. - * <p> - * As method parameter names are not available in the Geb artifacts we are delegating manually to - * get the best possible IDE support and user experience. - * - * @author Mattias Reichel - * @since 4.2 - */ -@CompileStatic -@SelfType(ContainerGebSpec) -trait PageDelegate implements Navigable, AlertAndConfirmSupport, WaitingSupport, FrameSupport, InteractionsSupport { - - @Delegate - private final TextMatchingSupport textMatchingSupport = new TextMatchingSupport() - - Page getPage() { - ContainerGebSpec.testManager.browser.page - } - - void to(Map params, UrlFragment fragment = null, Object[] args) { - page.to(params, fragment, args) - } - - UrlFragment getPageFragment() { - page.pageFragment - } - - String getPageUrl(String path) { - page.getPageUrl(path) - } - - String getTitle() { - page.title - } - - <T> T refreshWaitFor(Map params = [:], Closure<T> block) { - page.refreshWaitFor(params, block) - } - - <T> T refreshWaitFor(Map params = [:], String waitPreset, Closure<T> block) { - page.refreshWaitFor(params, waitPreset, block) - } - - <T> T refreshWaitFor(Map params = [:], Number timeoutSeconds, Closure<T> block) { - page.refreshWaitFor(params, timeoutSeconds, block) - } - - <T> T refreshWaitFor(Map params = [:], Number timeoutSeconds, Number intervalSeconds, Closure<T> block) { - page.refreshWaitFor(params, timeoutSeconds, intervalSeconds, block) - } - - @Override - Navigator find() { - page.find() - } - - @Override - Navigator $() { - page.$() - } - - @Override - Navigator find(int index) { - page.find(index) - } - - @Override - Navigator find(Range<Integer> range) { - page.find(range) - } - - @Override - Navigator $(int index) { - page.$(index) - } - - @Override - Navigator $(Range<Integer> range) { - page.$(range) - } - - @Override - Navigator $(Navigator... navigators) { - page.$(navigators) - } - - @Override - Navigator $(WebElement... elements) { - page.$(elements) - } - - @Override - Navigator focused() { - page.focused() - } - - @Override - <T extends Module> T module(Class<T> moduleClass) { - page.module(moduleClass) - } - - @Override - <T extends Module> T module(T module) { - page.module(module) - } - - @Override - Navigator find(String selector) { - page.find(selector) - } - - @Override - Navigator $(String selector) { - page.$(selector) - } - - @Override - Navigator find(Map<String, Object> attributes) { - page.find(attributes) - } - - @Override - Navigator find(String selector, int index) { - page.find(selector, index) - } - - @Override - Navigator find(String selector, Range<Integer> range) { - page.find(selector, range) - } - - @Override - Navigator $(String selector, int index) { - page.$(selector, index) - } - - @Override - Navigator $(String selector, Range<Integer> range) { - page.$(selector, range) - } - - @Override - Navigator $(Map<String, Object> attributes, By bySelector) { - page.$(attributes, bySelector) - } - - @Override - Navigator $(Map<String, Object> attributes, By bySelector, int index) { - page.$(attributes, bySelector, index) - } - - @Override - Navigator $(Map<String, Object> attributes, By bySelector, Range<Integer> range) { - page.$(attributes, bySelector, range) - } - - @Override - Navigator $(By bySelector) { - page.$(bySelector) - } - - @Override - Navigator $(By bySelector, int index) { - page.$(bySelector, index) - } - - @Override - Navigator $(By bySelector, Range<Integer> range) { - page.$(bySelector, range) - } - - @Override - Navigator find(By bySelector, Range<Integer> range) { - page.find(bySelector, range) - } - - @Override - Navigator $(Map<String, Object> attributes) { - page.$(attributes) - } - - @Override - Navigator $(Map<String, Object> attributes, int index) { - page.$(attributes, index) - } - - @Override - Navigator $(Map<String, Object> attributes, Range<Integer> range) { - page.$(attributes, range) - } - - @Override - Navigator $(Map<String, Object> attributes, String selector) { - page.$(attributes, selector) - } - - @Override - Navigator $(Map<String, Object> attributes, String selector, int index) { - page.$(attributes, selector, index) - } - - @Override - Navigator $(Map<String, Object> attributes, String selector, Range<Integer> range) { - page.$(attributes, selector, range) - } - - @Override - Navigator find(By bySelector) { - page.find(bySelector) - } - - @Override - Navigator find(By bySelector, int index) { - page.find(bySelector, index) - } - - @Override - Navigator find(Map<String, Object> attributes, By bySelector) { - page.find(attributes, bySelector) - } - - @Override - Navigator find(Map<String, Object> attributes, int index) { - page.find(attributes, index) - } - - @Override - Navigator find(Map<String, Object> attributes, Range<Integer> range) { - page.find(attributes, range) - } - - @Override - Navigator find(Map<String, Object> attributes, By bySelector, int index) { - page.find(attributes, bySelector, index) - } - - @Override - Navigator find(Map<String, Object> attributes, By bySelector, Range<Integer> range) { - page.find(attributes, bySelector, range) - } - - @Override - Navigator find(Map<String, Object> attributes, String selector) { - page.find(attributes, selector) - } - - @Override - Navigator find(Map<String, Object> attributes, String selector, int index) { - page.find(attributes, selector, index) - } - - @Override - Navigator find(Map<String, Object> attributes, String selector, Range<Integer> range) { - page.find(attributes, selector, range) - } - - @Override - Object withAlert(Closure actions) { - page.withAlert(actions) - } - - @Override - Object withAlert(Map params, Closure actions) { - page.withAlert(params, actions) - } - - @Override - void withNoAlert(Closure actions) { - page.withNoAlert(actions) - } - - @Override - Object withConfirm(boolean ok, Closure actions) { - page.withConfirm(ok, actions) - } - - @Override - Object withConfirm(Closure actions) { - page.withConfirm(actions) - } - - @Override - Object withConfirm(Map attributes, Closure actions) { - page.withConfirm(attributes, actions) - } - - @Override - Object withConfirm(Map attributes, boolean ok, Closure actions) { - page.withConfirm(attributes, ok, actions) - } - - @Override - void withNoConfirm(Closure actions) { - page.withNoConfirm(actions) - } - - @Override - <T> T waitFor(String waitPreset, Closure<T> block) { - page.waitFor(waitPreset, block) - } - - @Override - <T> T waitFor(Map params, String waitPreset, Closure<T> block) { - page.waitFor(params, waitPreset, block) - } - - @Override - <T> T waitFor(Closure<T> block) { - page.waitFor(block) - } - - @Override - <T> T waitFor(Map params, Closure<T> block) { - page.waitFor(params, block) - } - - @Override - <T> T waitFor(Number timeoutSeconds, Closure<T> block) { - page.waitFor(timeoutSeconds, block) - } - - @Override - <T> T waitFor(Map params, Number timeoutSeconds, Closure<T> block) { - page.waitFor(params, timeoutSeconds, block) - } - - @Override - <T> T waitFor(Number timeoutSeconds, Number intervalSeconds, Closure<T> block) { - page.waitFor(timeoutSeconds, intervalSeconds, block) - } - - @Override - <T> T waitFor(Map params, Number timeoutSeconds, Number intervalSeconds, Closure<T> block) { - page.waitFor(params, timeoutSeconds, intervalSeconds, block) - } - - @Override - <T> T withFrame(Object frame, Closure<T> block) { - page.withFrame(frame, block) - } - - @Override - <P extends Page, T> T withFrame(Object frame, @DelegatesTo.Target Class<P> page, @DelegatesTo(strategy = 1, genericTypeIndex = 0) Closure<T> block) { - this.page.withFrame(frame, page, block) - } - - @Override - <P extends Page, T> T withFrame(Object frame, @DelegatesTo.Target P page, @DelegatesTo(strategy = 1) Closure<T> block) { - this.page.withFrame(frame, page, block) - } - - @Override - <P extends Page, T> T withFrame(Navigator frame, @DelegatesTo.Target Class<P> page, @DelegatesTo(strategy = 1, genericTypeIndex = 0) Closure<T> block) { - this.page.withFrame(frame, page, block) - } - - @Override - <P extends Page, T> T withFrame(Navigator frame, @DelegatesTo.Target P page, @DelegatesTo(strategy = 1) Closure<T> block) { - this.page.withFrame(frame, page, block) - } - - @Override - <T> T withFrame(Navigator frame, Closure<T> block) { - page.withFrame(frame, block) - } - - @Override - <T> T withFrame(TemplateDerivedPageContent frame, Closure<T> block) { - page.withFrame(frame, block) - } - - @Override - void interact(@DelegatesTo(strategy = 1, value = InteractDelegate) Closure interactionClosure) { - page.interact(interactionClosure) - } -} \ No newline at end of file diff --git a/integration/geb-testcontainers/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/integration/geb-testcontainers/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension index beedcb82..da620020 100644 --- a/integration/geb-testcontainers/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension +++ b/integration/geb-testcontainers/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension @@ -1 +1 @@ -geb.testcontainers.ContainerGebExtension \ No newline at end of file +geb.testcontainers.ContainerGebExtension
