From 5cf44146ec9e493c81831437973fe9b7caae1d65 Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Sat, 23 Nov 2024 10:12:18 -0500 Subject: [PATCH] feature #79 - add recording support --- README.md | 25 ++++++++++ .../geb/ContainerGebConfiguration.groovy | 44 +++++++++++++++++ .../geb/ContainerGebRecordingExtension.groovy | 47 +++++++++++++++++++ .../grails/plugin/geb/ContainerGebSpec.groovy | 14 ++---- .../geb/ContainerGebTestDescription.groovy | 19 ++++++++ .../geb/ContainerGebTestListener.groovy | 32 +++++++++++++ ...amework.runtime.extension.IGlobalExtension | 1 + 7 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy create mode 100644 src/testFixtures/groovy/grails/plugin/geb/ContainerGebRecordingExtension.groovy create mode 100644 src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy create mode 100644 src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestListener.groovy create mode 100644 src/testFixtures/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension diff --git a/README.md b/README.md index 5db4161..df53bad 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,31 @@ This requires a [compatible container runtime](https://java.testcontainers.org/s If you choose to use the `ContainerGebSpec` class, as long as you have a compatible container runtime installed, you don't need to do anything else. Just run `./gradlew integrationTest` and a container will be started and configured to start a browser that can access your application under test. +#### Recording +By default, all failed tests will generate a video recording of the test execution. These recordings are saved to a `recordings` directory under the project's `build` directory. + +The following system properties can be change to configure the recording behavior: +* `grails.geb.recording.enabled` + * purpose: toggle for recording + * possible values: `true` or `false` + + +* `grails.geb.recording.directory` + * purpose: the directory to save the recordings relative to the project directory + * defaults to `build/recordings` + + +* `grails.geb.recording.mode` + * purpose: which tests to record via the enum `VncRecordingMode` + * possible values: `RECORD_ALL` or `RECORD_FAILING` + * defaults to `RECORD_FAILING` + + +* `grails.geb.recording.format` + * purpose: sets the format of the recording + * possible values are `FLV` or `MP4` + * defaults to `MP4` + ### GebSpec If you choose to extend `GebSpec`, you will need to have a [Selenium WebDriver](https://www.selenium.dev/documentation/webdriver/browsers/) installed that matches a browser you have installed on your system. diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy new file mode 100644 index 0000000..5cc1581 --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebConfiguration.groovy @@ -0,0 +1,44 @@ +package grails.plugin.geb + +import groovy.transform.CompileStatic +import groovy.transform.Memoized +import groovy.util.logging.Slf4j +import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.containers.VncRecordingContainer + +@Slf4j +@CompileStatic +class ContainerGebConfiguration { + String recordingDirectoryName + + boolean recording + + BrowserWebDriverContainer.VncRecordingMode recordingMode + + VncRecordingContainer.VncRecordingFormat recordingFormat + + ContainerGebConfiguration() { + recording = Boolean.parseBoolean(System.getProperty('grails.geb.recording.enabled', true as String)) + recordingDirectoryName = System.getProperty('grails.geb.recording.directory', 'build/recordings') + recordingMode = BrowserWebDriverContainer.VncRecordingMode.valueOf(System.getProperty('grails.geb.recording.mode', BrowserWebDriverContainer.VncRecordingMode.RECORD_FAILING.name())) + recordingFormat = VncRecordingContainer.VncRecordingFormat.valueOf(System.getProperty('grails.geb.recording.format', VncRecordingContainer.VncRecordingFormat.MP4.name())) + } + + @Memoized + File getRecordingDirectory() { + if(!recording) { + return null + } + + File recordingDirectory = new File(recordingDirectoryName) + if(!recordingDirectory.exists()) { + log.info("Could not find `${recordingDirectoryName}` directory for recording. Creating...") + recordingDirectory.mkdir() + } + else if(!recordingDirectory.isDirectory()) { + throw new IllegalStateException("Recording Directory name expected to be `${recordingDirectoryName}, but found file instead.") + } + + recordingDirectory + } +} diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebRecordingExtension.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebRecordingExtension.groovy new file mode 100644 index 0000000..2c60b9d --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebRecordingExtension.groovy @@ -0,0 +1,47 @@ +package grails.plugin.geb + +import groovy.transform.TailRecursive +import groovy.util.logging.Slf4j +import org.spockframework.runtime.extension.IGlobalExtension +import org.spockframework.runtime.model.SpecInfo + +import java.time.LocalDateTime + +@Slf4j +class ContainerGebRecordingExtension implements IGlobalExtension { + ContainerGebConfiguration configuration + + @Override + void start() { + configuration = new ContainerGebConfiguration() + } + + @Override + void visitSpec(SpecInfo spec) { + if (isContainerizedGebSpec(spec)) { + ContainerGebTestListener listener = new ContainerGebTestListener(spec, LocalDateTime.now()) + // TODO: We should initialize the web driver container once for all geb tests so we don't have to spin it up & down. + spec.addSetupInterceptor { + ContainerGebSpec gebSpec = it.instance as ContainerGebSpec + gebSpec.initialize() + if(configuration.recording) { + listener.webDriverContainer = gebSpec.webDriverContainer.withRecordingMode(configuration.recordingMode, configuration.recordingDirectory, configuration.recordingFormat) + } + } + + spec.addListener(listener) + } + } + + @TailRecursive + boolean isContainerizedGebSpec(SpecInfo spec) { + if(spec != null) { + if(spec.filename.startsWith('ContainerGebSpec.')) { + return true + } + + return isContainerizedGebSpec(spec.superSpec) + } + return false + } +} diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy index 82a927d..b0ddd54 100644 --- a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy @@ -72,6 +72,10 @@ abstract class ContainerGebSpec extends Specification implements ManagedGebTest, @PackageScope void initialize() { + if(webDriverContainer) { + return + } + webDriverContainer = new BrowserWebDriverContainer() Testcontainers.exposeHostPorts(port) webDriverContainer.tap { @@ -85,12 +89,6 @@ abstract class ContainerGebSpec extends Specification implements ManagedGebTest, WebDriver driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions()) driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)) browser.driver = driver - } - - void setup() { - if (notInitialized) { - initialize() - } browser.baseUrl = "$protocol://$hostName:$port" } @@ -164,8 +162,4 @@ abstract class ContainerGebSpec extends Specification implements ManagedGebTest, private boolean isHostNameChanged() { return hostNameFromContainer != DEFAULT_HOSTNAME_FROM_CONTAINER } - - private boolean isNotInitialized() { - webDriverContainer == null - } } \ No newline at end of file diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy new file mode 100644 index 0000000..214d9a8 --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestDescription.groovy @@ -0,0 +1,19 @@ +package grails.plugin.geb + +import org.spockframework.runtime.model.IterationInfo +import org.testcontainers.lifecycle.TestDescription + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class ContainerGebTestDescription implements TestDescription { + String testId + String filesystemFriendlyName + + ContainerGebTestDescription(IterationInfo testInfo, LocalDateTime runDate) { + testId = testInfo.displayName + + String safeName = testId.replaceAll("\\W+", "") + filesystemFriendlyName = "${DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(runDate)}_${safeName}" + } +} diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestListener.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestListener.groovy new file mode 100644 index 0000000..9cb01db --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebTestListener.groovy @@ -0,0 +1,32 @@ +package grails.plugin.geb + +import org.spockframework.runtime.AbstractRunListener +import org.spockframework.runtime.model.ErrorInfo +import org.spockframework.runtime.model.IterationInfo +import org.spockframework.runtime.model.SpecInfo +import org.testcontainers.containers.BrowserWebDriverContainer + +import java.time.LocalDateTime + +class ContainerGebTestListener extends AbstractRunListener { + BrowserWebDriverContainer webDriverContainer + ErrorInfo errorInfo + SpecInfo spec + LocalDateTime runDate + + ContainerGebTestListener(SpecInfo spec, LocalDateTime runDate) { + this.spec = spec + this.runDate = runDate + } + + @Override + void afterIteration(IterationInfo iteration) { + webDriverContainer.afterTest(new ContainerGebTestDescription(iteration, runDate), Optional.of(errorInfo?.exception)) + errorInfo = null + } + + @Override + void error(ErrorInfo error) { + errorInfo = error + } +} \ No newline at end of file diff --git a/src/testFixtures/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/src/testFixtures/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension new file mode 100644 index 0000000..893fab6 --- /dev/null +++ b/src/testFixtures/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension @@ -0,0 +1 @@ +grails.plugin.geb.ContainerGebRecordingExtension \ No newline at end of file