Skip to content

Commit

Permalink
feature grails#79 - add recording support
Browse files Browse the repository at this point in the history
  • Loading branch information
jdaugherty committed Nov 23, 2024
1 parent c459b49 commit 5cf4414
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 10 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
14 changes: 4 additions & 10 deletions src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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}"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
grails.plugin.geb.ContainerGebRecordingExtension

0 comments on commit 5cf4414

Please sign in to comment.