From 3137347f830193069a2a12712f0c984fd934b51f Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Thu, 20 Aug 2026 12:05:26 -0500 Subject: [PATCH] Add BrowserWebDriverContainer#restartVncRecording() for per-test recordings Restarting a browser container's VNC recorder mid-lifecycle has no public API today, forcing anyone who reuses a single BrowserWebDriverContainer across multiple tests (e.g. to avoid paying for a fresh browser session per test) to reach into the private vncRecordingContainer field via reflection to get a separate recording per test. This has been an open ask since #3998, with maintainers and users converging on exactly this shape of fix in that issue's discussion. restartVncRecording() stops the current recording container and starts a fresh one, clearing the field before the replacement starts so a failed start() can't leave a stale reference to an already-stopped container in place for afterTest() to save from - and stops the replacement's container explicitly on a failed start so it isn't orphaned. As a companion safety net, retainRecordingIfNeeded() now guards against a null vncRecordingContainer instead of throwing a NullPointerException that would otherwise propagate out of afterTest(). Verified against a real Docker daemon: two calls to afterTest() separated by a restartVncRecording() call now produce two distinct recording files instead of one continuous recording. Closes #3998 --- docs/modules/webdriver_containers.md | 8 +++ .../selenium/BrowserWebDriverContainer.java | 52 ++++++++++++++++ ...ChromeRecordingWebDriverContainerTest.java | 61 +++++++++++++++---- 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/docs/modules/webdriver_containers.md b/docs/modules/webdriver_containers.md index 8101f489e3a..a7771d24eab 100644 --- a/docs/modules/webdriver_containers.md +++ b/docs/modules/webdriver_containers.md @@ -80,6 +80,14 @@ If you would like to customise the file name of the recording, or provide a diff Note the factory must implement `org.testcontainers.containers.RecordingFileFactory`. +If you reuse a single `BrowserWebDriverContainer` across multiple tests (e.g. to avoid the cost of starting a new +browser container per test), call `restartVncRecording()` before each test so that `afterTest()` saves a separate +recording per test instead of one continuous recording for the whole container's lifetime: + + +[Restart recording between tests](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:restart + + ## More examples A few different examples are shown in [ChromeWebDriverContainerTest.java](https://github.com/testcontainers/testcontainers-java/blob/main/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java). diff --git a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java index 97ac23f5d55..5ca00c6c05f 100644 --- a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java @@ -201,6 +201,52 @@ public void afterTest(TestDescription description, Optional throwable retainRecordingIfNeeded(description.getFilesystemFriendlyName(), !throwable.isPresent()); } + /** + * Restarts VNC recording, so that a separate recording is captured for each test method when a single + * {@link BrowserWebDriverContainer} instance is reused across multiple tests (e.g. to avoid the cost of + * starting a new browser container per test). Call this before each test starts; {@link #afterTest} will then + * save the recording captured since the last restart. + *

+ * Does nothing if recording is not enabled ({@link VncRecordingMode#SKIP}) or the container has not started yet. + * + * @throws ContainerLaunchException if the replacement recording container fails to start. The previous + * recording container is stopped regardless, so recording is disabled (as if {@link VncRecordingMode#SKIP} had + * been used) rather than left in a stale or partially-started state. + */ + public void restartVncRecording() { + if (recordingMode == VncRecordingMode.SKIP || vncRecordingContainer == null) { + return; + } + + VncRecordingContainer previousRecordingContainer = vncRecordingContainer; + // Clear the field before starting the replacement below: if start() throws, a stale reference to this + // now-stopped container must not be left in place for afterTest() to save from. + vncRecordingContainer = null; + try { + previousRecordingContainer.stop(); + } catch (Exception e) { + LOGGER.debug("Failed to stop vncRecordingContainer", e); + } + + VncRecordingContainer nextRecordingContainer = new VncRecordingContainer(this) + .withVncPassword(DEFAULT_PASSWORD) + .withVncPort(VNC_PORT) + .withVideoFormat(recordingFormat); + try { + nextRecordingContainer.start(); + } catch (Exception e) { + // start() may have already created the underlying container (e.g. its wait strategy timed out) - + // stop it explicitly so it isn't left running until Ryuk reaps it. + try { + nextRecordingContainer.stop(); + } catch (Exception stopException) { + e.addSuppressed(stopException); + } + throw new ContainerLaunchException("Failed to restart VNC recording container", e); + } + vncRecordingContainer = nextRecordingContainer; + } + @Override public void stop() { if (vncRecordingContainer != null) { @@ -230,6 +276,12 @@ private void retainRecordingIfNeeded(String prefix, boolean succeeded) { } if (shouldRecord) { + if (vncRecordingContainer == null) { + // Can happen if restartVncRecording() failed to start a replacement recording container. + LOGGER.warn("No VNC recording container available for test {} - recording will not be saved", prefix); + return; + } + File recordingFile = recordingFileFactory.recordingFileForTest( vncRecordingDirectory, prefix, diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java index dbb39b86599..c03376dcbe8 100644 --- a/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java @@ -63,23 +63,62 @@ private File[] runSimpleExploreInContainer(BrowserWebDriverContainer container, TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); doSimpleExplore(container, new ChromeOptions()); container.afterTest( - new TestDescription() { - @Override - public String getTestId() { - return getFilesystemFriendlyName(); - } - - @Override - public String getFilesystemFriendlyName() { - return "ChromeThatRecordsAllTests-recordingTestThatShouldBeRecordedAndRetained"; - } - }, + testDescription("ChromeThatRecordsAllTests-recordingTestThatShouldBeRecordedAndRetained"), Optional.empty() ); return vncRecordingDirectory.toFile().listFiles(new PatternFilenameFilter(fileNamePattern)); } + private TestDescription testDescription(String filesystemFriendlyName) { + return new TestDescription() { + @Override + public String getTestId() { + return getFilesystemFriendlyName(); + } + + @Override + public String getFilesystemFriendlyName() { + return filesystemFriendlyName; + } + }; + } + + @Test + void restartVncRecordingProducesASeparateFileForEachTest() throws InterruptedException { + File target = vncRecordingDirectory.toFile(); + try ( + // restart { + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") + .withRecordingMode(VncRecordingMode.RECORD_ALL, target) + .withRecordingFileFactory(new DefaultRecordingFileFactory()) + .withNetwork(NETWORK) + ) { + chrome.start(); + + TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); + doSimpleExplore(chrome, new ChromeOptions()); + chrome.afterTest( + testDescription("restartVncRecordingProducesASeparateFileForEachTest-first"), + Optional.empty() + ); + + // Call this before each subsequent test so its recording doesn't get appended to the previous one + chrome.restartVncRecording(); + // } + + TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); + doSimpleExplore(chrome, new ChromeOptions()); + chrome.afterTest( + testDescription("restartVncRecordingProducesASeparateFileForEachTest-second"), + Optional.empty() + ); + + File[] files = vncRecordingDirectory.toFile().listFiles(new PatternFilenameFilter("PASSED-.*\\.flv")); + assertThat(files).as("a separate recording file exists per test").hasSize(2); + } + } + @Test void recordingTestShouldHaveFlvExtension() throws InterruptedException { File target = vncRecordingDirectory.toFile();