Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature #79 - recording support #80

Merged
merged 19 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,66 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.transform.PackageScope
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
import groovy.util.logging.Slf4j
import org.testcontainers.containers.BrowserWebDriverContainer
import org.testcontainers.containers.VncRecordingContainer

/**
* A container class to parse the configuration used by {@link grails.plugin.geb.ContainerGebRecordingExtension}
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
*
* @author James Daugherty
* @since 5.0.0
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
*/
@Slf4j
@CompileStatic
class ContainerGebConfiguration {
String recordingDirectoryName

boolean recording

BrowserWebDriverContainer.VncRecordingMode recordingMode

VncRecordingContainer.VncRecordingFormat recordingFormat
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved

ContainerGebConfiguration() {
recording = Boolean.parseBoolean(System.getProperty('grails.geb.recording.enabled', true as String))
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
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) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
return null
}

File recordingDirectory = new File(recordingDirectoryName)
if(!recordingDirectory.exists()) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
log.info("Could not find `${recordingDirectoryName}` directory for recording. Creating...")
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
recordingDirectory.mkdir()
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
else if(!recordingDirectory.isDirectory()) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalStateException("Recording Directory name expected to be `${recordingDirectoryName}, but found file instead.")
}

recordingDirectory
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import groovy.transform.CompileStatic
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

/**
* A Spock Extension that manages the Testcontainers lifecycle for a {@link grails.plugin.geb.ContainerGebSpec}
*
* @author James Daugherty
* @since 5.0.0
*/
@Slf4j
@CompileStatic
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()
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
if(configuration.recording) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
listener.webDriverContainer = gebSpec.webDriverContainer.withRecordingMode(configuration.recordingMode, configuration.recordingDirectory, configuration.recordingFormat)
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
}
spec.addCleanupInterceptor {
ContainerGebSpec gebSpec = it.instance as ContainerGebSpec
gebSpec.container?.stop()
}

spec.addListener(listener)
}
}

@TailRecursive
private boolean isContainerizedGebSpec(SpecInfo spec) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
if(spec != null) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
if(spec.filename.startsWith('ContainerGebSpec.')) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
return true
}

jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
return isContainerizedGebSpec(spec.superSpec)
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
return false
}
}
18 changes: 4 additions & 14 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) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
return
}

webDriverContainer = new BrowserWebDriverContainer()
Testcontainers.exposeHostPorts(port)
webDriverContainer.tap {
Expand All @@ -85,19 +89,9 @@ 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()
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
browser.baseUrl = "$protocol://$hostName:$port"
}

def cleanupSpec() {
webDriverContainer?.stop()
}

/**
* Get access to container running the web-driver, for convenience to execInContainer, copyFileToContainer etc.
*
Expand Down Expand Up @@ -164,8 +158,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,44 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import groovy.transform.CompileStatic
import org.spockframework.runtime.model.IterationInfo
import org.testcontainers.lifecycle.TestDescription

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
* Implements {@link org.testcontainers.lifecycle.TestDescription} to customize recording names.
*
* @see org.testcontainers.lifecycle.TestDescription
*
* @author James Daugherty
* @since 5.0
*/
@CompileStatic
class ContainerGebTestDescription implements TestDescription {
String testId
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
String filesystemFriendlyName

ContainerGebTestDescription(IterationInfo testInfo, LocalDateTime runDate) {
testId = testInfo.displayName

jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
String safeName = testId.replaceAll("\\W+", "")
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
filesystemFriendlyName = "${DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(runDate)}_${safeName}"
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import groovy.transform.CompileStatic
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
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved

import java.time.LocalDateTime

/**
* A test listener that reports the test result to {@link org.testcontainers.containers.BrowserWebDriverContainer} so
* that recordings may be saved.
*
* @see org.testcontainers.containers.BrowserWebDriverContainer#afterTest
*
* @author James Daugherty
* @since 5.0
*/
@CompileStatic
class ContainerGebTestListener extends AbstractRunListener {
BrowserWebDriverContainer webDriverContainer
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
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))
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
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
Loading