for the full
+ * license.
+ */
+package com.vaadin.testbench.loadtest;
+
+import java.nio.file.Paths;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserContext;
+
+/**
+ * Static helper methods for Playwright-based integration tests.
+ *
+ * Provides browser context creation with optional HAR recording and base URL
+ * resolution.
+ *
+ * HAR recording for k6 load test generation is activated transparently by the
+ * {@code loadtest:record-playwright} Maven goal — no test code changes needed.
+ */
+public final class PlaywrightHelper {
+
+ private PlaywrightHelper() {
+ }
+
+ /**
+ * Creates a BrowserContext. When the k6 Maven plugin runs a test for
+ * recording, it sets a system property that enables HAR capture
+ * automatically — the test itself does not need to know about this.
+ */
+ public static BrowserContext createBrowserContext(Browser browser) {
+ String harOutputPath = System.getProperty("k6.harOutputPath");
+ if (harOutputPath != null && !harOutputPath.isEmpty()) {
+ return browser.newContext(new Browser.NewContextOptions()
+ .setRecordHarPath(Paths.get(harOutputPath))
+ .setRecordHarMode(
+ com.microsoft.playwright.options.HarMode.FULL));
+ }
+ return browser.newContext();
+ }
+
+ /**
+ * Returns the base URL for the deployment under test, resolved from
+ * environment variables and system properties.
+ */
+ public static String getBaseUrl() {
+ return "http://" + getDeploymentHostname() + ":" + getDeploymentPort();
+ }
+
+ private static String getDeploymentHostname() {
+ String hostname = System.getenv("HOSTNAME");
+ if (hostname != null && !hostname.isEmpty()) {
+ return hostname;
+ }
+ return "localhost";
+ }
+
+ private static int getDeploymentPort() {
+ String port = System.getProperty("server.port");
+ if (port != null && !port.isEmpty()) {
+ return Integer.parseInt(port);
+ }
+ return 8080;
+ }
+}
diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractK6Mojo.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractK6Mojo.java
index 036b728f4..545b2449f 100644
--- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractK6Mojo.java
+++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractK6Mojo.java
@@ -34,6 +34,8 @@
*/
public abstract class AbstractK6Mojo extends AbstractMojo {
+ static final String CONTENT_BREAK = "========================================";
+
/**
* The Maven project.
*/
diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RecordMojo.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractRecordMojo.java
similarity index 58%
rename from vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RecordMojo.java
rename to vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractRecordMojo.java
index 5d56c4712..5635f64d0 100644
--- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RecordMojo.java
+++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/AbstractRecordMojo.java
@@ -10,6 +10,7 @@
import java.io.BufferedReader;
import java.io.File;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -20,89 +21,69 @@
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import com.vaadin.testbench.loadtest.util.K6TestRefactorer.ThinkTimeConfig;
import com.vaadin.testbench.loadtest.util.SourceHasher;
/**
- * Records TestBench tests through a proxy and converts them to k6 load tests.
- *
- * This goal supports recording multiple test classes. For each test class it:
- *
- * - Starts a recording proxy on the specified port
- * - Runs the TestBench test class with proxy configuration
- * - Stops the proxy and saves the HAR file
- * - Converts the HAR to a k6 test (filters, converts, refactors)
- *
- *
- * Example usage:
- *
- *
- * mvn k6:record -Dk6.testClasses=HelloWorldIT,CrudExampleIT
- *
+ * Base class for recording test scenarios and converting them to k6 load tests.
+ * Provides the shared orchestration for cache checking, HAR post-processing,
+ * and k6 script generation. Subclasses implement the recording mechanism
+ * (proxy-based for TestBench, native HAR for Playwright).
*/
-@Mojo(name = "record", defaultPhase = LifecyclePhase.INTEGRATION_TEST)
-public class K6RecordMojo extends AbstractK6Mojo {
+public abstract class AbstractRecordMojo extends AbstractK6Mojo {
/**
- * The TestBench test class to record. Can be a simple class name or fully
- * qualified name. For multiple classes, use testClasses instead.
+ * The test class to record. Can be a simple class name or fully qualified
+ * name. For multiple classes, use testClasses instead.
*/
@Parameter(property = "k6.testClass")
- private String testClass;
+ protected String testClass;
/**
- * List of TestBench test classes to record. Each class will be recorded
- * separately and generate its own k6 test file.
+ * List of test classes to record. Each class will be recorded separately
+ * and generate its own k6 test file.
*/
@Parameter(property = "k6.testClasses")
- private List testClasses;
-
- /**
- * Port for the recording proxy.
- */
- @Parameter(property = "k6.proxyPort", defaultValue = "6000")
- private int proxyPort;
+ protected List testClasses;
/**
* Port where the application is running.
*/
@Parameter(property = "k6.appPort", defaultValue = "8080")
- private int appPort;
+ protected int appPort;
/**
- * Working directory for running the TestBench test. Defaults to the project
- * base directory.
+ * Working directory for running the test. Defaults to the project base
+ * directory.
*/
@Parameter(property = "k6.testWorkDir", defaultValue = "${project.basedir}")
- private File testWorkDir;
+ protected File testWorkDir;
/**
* Directory to store HAR recordings.
*/
@Parameter(property = "k6.harDir", defaultValue = "${project.build.directory}")
- private File harDir;
+ protected File harDir;
/**
* Output directory for generated k6 tests.
*/
@Parameter(property = "k6.outputDir", defaultValue = "${project.build.directory}/k6/tests")
- private File outputDir;
+ protected File outputDir;
/**
- * Timeout for TestBench test execution in seconds.
+ * Timeout for test execution in seconds.
*/
@Parameter(property = "k6.testTimeout", defaultValue = "300")
- private int testTimeout;
+ protected int testTimeout;
/**
- * Additional Maven arguments for running the TestBench test.
+ * Additional Maven arguments for running the test.
*/
@Parameter(property = "k6.mavenArgs")
- private String mavenArgs;
+ protected String mavenArgs;
/**
* Force re-recording even if sources haven't changed. By default, recording
@@ -110,7 +91,7 @@ public class K6RecordMojo extends AbstractK6Mojo {
* last recording.
*/
@Parameter(property = "k6.forceRecord", defaultValue = "false")
- private boolean forceRecord;
+ protected boolean forceRecord;
/**
* Enable realistic think time delays between user actions. When enabled,
@@ -119,7 +100,7 @@ public class K6RecordMojo extends AbstractK6Mojo {
* maximum throughput testing.
*/
@Parameter(property = "k6.thinkTime.enabled", defaultValue = "true")
- private boolean thinkTimeEnabled;
+ protected boolean thinkTimeEnabled;
/**
* Base delay in seconds after page load (v-r=init response). Simulates time
@@ -128,7 +109,7 @@ public class K6RecordMojo extends AbstractK6Mojo {
* delays while keeping interaction delays.
*/
@Parameter(property = "k6.thinkTime.pageReadDelay", defaultValue = "2.0")
- private double pageReadDelay;
+ protected double pageReadDelay;
/**
* Base delay in seconds after user interaction (v-r=uidl response).
@@ -137,30 +118,60 @@ public class K6RecordMojo extends AbstractK6Mojo {
* delays while keeping page read delays.
*/
@Parameter(property = "k6.thinkTime.interactionDelay", defaultValue = "0.5")
- private double interactionDelay;
+ protected double interactionDelay;
+
+ protected SourceHasher sourceHasher;
+
+ /**
+ * Returns the goal name for logging (e.g., "k6:record").
+ */
+ protected abstract String getGoalName();
+
+ /**
+ * Returns the test framework name for logging (e.g., "TestBench").
+ */
+ protected abstract String getTestFrameworkName();
+
+ /**
+ * Logs recording-specific configuration after the common header.
+ */
+ protected abstract void logRecordingConfiguration();
- private SourceHasher sourceHasher;
+ /**
+ * Records a HAR file by running the test. Must ensure a valid HAR file
+ * exists at {@code harPath} when returning, or throw an exception.
+ *
+ * @param currentTestClass
+ * the test class to record
+ * @param harPath
+ * where the HAR file should be written
+ * @throws MojoExecutionException
+ * if recording fails
+ * @throws InterruptedException
+ * if interrupted during recording
+ * @throws IOException
+ * if an I/O error occurs
+ */
+ protected abstract void recordHar(String currentTestClass, Path harPath)
+ throws MojoExecutionException, InterruptedException, IOException;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
- getLog().info("Skipping k6:record");
+ getLog().info("Skipping " + getGoalName());
return;
}
- // Build list of test classes to record
List classesToRecord = getTestClassesToRecord();
if (classesToRecord.isEmpty()) {
throw new MojoExecutionException(
"No test classes specified. Use testClass or testClasses parameter.");
}
- getLog().info(
- "Recording " + classesToRecord.size() + " TestBench test(s)");
- getLog().info(" Proxy port: " + proxyPort);
- getLog().info(" App port: " + appPort);
+ getLog().info("Recording " + classesToRecord.size() + " "
+ + getTestFrameworkName() + " test(s)");
+ logRecordingConfiguration();
- // Initialize (extract utilities, validate prerequisites)
initialize();
sourceHasher = new SourceHasher();
@@ -170,7 +181,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
List generatedTests = new ArrayList<>();
List cachedTests = new ArrayList<>();
- // Record each test class
for (String currentTestClass : classesToRecord) {
try {
RecordResult result = recordSingleTest(currentTestClass,
@@ -188,7 +198,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
}
}
- // Copy Vaadin helpers once
copyVaadinHelpers(outputPath);
getLog().info("");
@@ -227,7 +236,6 @@ private record RecordResult(Path testFile, boolean wasCached) {
private List getTestClassesToRecord() {
List result = new ArrayList<>();
- // Add from testClasses list
if (testClasses != null && !testClasses.isEmpty()) {
for (String tc : testClasses) {
// Support comma-separated values in list items
@@ -241,7 +249,6 @@ private List getTestClassesToRecord() {
// Add single testClass if specified and not already in list
if (testClass != null && !testClass.isEmpty()) {
- // Support comma-separated values
for (String tc : testClass.split("\\s*,\\s*")) {
if (!result.contains(tc.trim())) {
result.add(tc.trim());
@@ -258,10 +265,9 @@ private List getTestClassesToRecord() {
* changed.
*/
private RecordResult recordSingleTest(String currentTestClass,
- Path outputPath) throws MojoExecutionException,
- InterruptedException, java.io.IOException {
+ Path outputPath)
+ throws MojoExecutionException, InterruptedException, IOException {
- // Prepare paths
String outputName = scenarioToFileName(currentTestClass);
Path harPath = harDir.toPath().resolve(outputName + "-recording.har")
.toAbsolutePath();
@@ -299,121 +305,80 @@ private RecordResult recordSingleTest(String currentTestClass,
getLog().warn("Could not clean up old files: " + e.getMessage());
}
- try {
- // Step 1: Start proxy recorder
- nodeRunner.startProxyRecorder(proxyPort, harPath);
-
- // Step 2: Run TestBench test
- boolean testSuccess = runTestBenchTest(currentTestClass);
-
- // Step 3: Verify proxy captured traffic before stopping
- int recordedEntries = nodeRunner.getRecordedEntryCount();
- if (recordedEntries == 0) {
- nodeRunner.stopProxyRecorder();
- throw new MojoExecutionException(
- "Proxy recorded 0 requests for test '"
- + currentTestClass + "'. "
- + "The test likely did not route traffic through the proxy on port "
- + proxyPort + ". "
- + "Verify that the K6RecordingExtension is on the classpath and "
- + "junit.jupiter.extensions.autodetection.enabled=true is set.");
- }
- getLog().info("Proxy captured " + recordedEntries + " requests");
-
- // Step 4: Stop proxy (this writes the HAR file)
- nodeRunner.stopProxyRecorder();
-
- // Wait a moment for HAR file to be written
- Thread.sleep(1000);
-
- // Check HAR file
- if (!Files.exists(harPath)) {
- throw new MojoExecutionException(
- "HAR file was not created: " + harPath);
- }
+ // Record HAR (subclass-specific)
+ recordHar(currentTestClass, harPath);
- getLog().info("HAR file created: " + harPath + " ("
- + Files.size(harPath) + " bytes)");
+ // Post-processing (shared)
+ nodeRunner.filterHar(harPath);
+ nodeRunner.harToK6(harPath, generatedFile, buildThresholdConfig());
- if (!testSuccess) {
- getLog().warn(
- "TestBench test may have failed, but HAR was recorded. Continuing with conversion...");
- }
+ ThinkTimeConfig thinkTimeConfig = new ThinkTimeConfig(thinkTimeEnabled,
+ pageReadDelay, interactionDelay);
+ nodeRunner.refactorK6Test(generatedFile, refactoredFile,
+ thinkTimeConfig);
- // Step 4: Filter external domains
- nodeRunner.filterHar(harPath);
-
- // Step 5: Convert HAR to k6 (with configurable thresholds)
- nodeRunner.harToK6(harPath, generatedFile, buildThresholdConfig());
+ // Store hash for future cache checks
+ String currentHash = sourceHasher
+ .calculateSourceHash(testWorkDir.toPath(), currentTestClass);
+ if (currentHash != null) {
+ sourceHasher.storeHash(hashFile, currentHash);
+ }
- // Step 6: Refactor for Vaadin (with think time configuration)
- ThinkTimeConfig thinkTimeConfig = new ThinkTimeConfig(
- thinkTimeEnabled, pageReadDelay, interactionDelay);
- nodeRunner.refactorK6Test(generatedFile, refactoredFile,
- thinkTimeConfig);
+ return new RecordResult(refactoredFile, false);
+ }
- // Step 7: Store hash for future cache checks
- String currentHash = sourceHasher.calculateSourceHash(
- testWorkDir.toPath(), currentTestClass);
- if (currentHash != null) {
- sourceHasher.storeHash(hashFile, currentHash);
+ /**
+ * Builds the base Maven failsafe command with common arguments. The
+ * returned list is mutable so subclasses can add framework-specific
+ * arguments.
+ *
+ * @param currentTestClass
+ * the test class to run
+ * @return the command list
+ */
+ protected List buildBaseTestCommand(String currentTestClass) {
+ List command = new ArrayList<>();
+ boolean isWindows = System.getProperty("os.name", "").toLowerCase()
+ .contains("win");
+ if (isWindows) {
+ command.add("cmd.exe");
+ command.add("/c");
+ }
+ command.add("mvn");
+ command.add("failsafe:integration-test");
+ command.add("-Dit.test=" + currentTestClass);
+ command.add("-Dserver.port=" + appPort);
+ command.add("-DfailIfNoTests=false");
+
+ if (mavenArgs != null && !mavenArgs.isEmpty()) {
+ for (String arg : mavenArgs.split("\\s+")) {
+ command.add(arg);
}
-
- return new RecordResult(refactoredFile, false);
-
- } finally {
- // Ensure proxy is stopped
- nodeRunner.stopProxyRecorder();
}
+
+ return command;
}
/**
- * Runs the TestBench test using Maven failsafe plugin.
+ * Runs a Maven failsafe test with the given command.
*
- * @param currentTestClass
- * the test class to run
+ * @param command
+ * the full command to execute
* @return true if the test completed successfully
* @throws MojoExecutionException
* if test execution fails critically
*/
- private boolean runTestBenchTest(String currentTestClass)
+ protected boolean runMavenTest(List command)
throws MojoExecutionException {
- getLog().info("Running TestBench test: " + currentTestClass);
+ getLog().debug("Test command: " + String.join(" ", command));
try {
- List command = new ArrayList<>();
- boolean isWindows = System.getProperty("os.name", "").toLowerCase()
- .contains("win");
- if (isWindows) {
- command.add("cmd.exe");
- command.add("/c");
- }
- command.add("mvn");
- command.add("failsafe:integration-test");
- command.add("-Dit.test=" + currentTestClass);
- command.add("-Dk6.proxy.host=localhost:" + proxyPort);
- command.add("-Dserver.port=" + appPort);
- command.add("-DfailIfNoTests=false");
- // Enable JUnit 5 auto-detection for K6RecordingExtension
- command.add(
- "-Djunit.jupiter.extensions.autodetection.enabled=true");
-
- // Add any extra Maven arguments
- if (mavenArgs != null && !mavenArgs.isEmpty()) {
- for (String arg : mavenArgs.split("\\s+")) {
- command.add(arg);
- }
- }
-
- getLog().debug("Test command: " + String.join(" ", command));
-
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(testWorkDir);
pb.redirectErrorStream(true);
Process process = pb.start();
- // Stream output
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
@@ -425,21 +390,21 @@ private boolean runTestBenchTest(String currentTestClass)
boolean finished = process.waitFor(testTimeout, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
- getLog().warn("TestBench test timed out after " + testTimeout
- + " seconds");
+ getLog().warn(
+ "Test timed out after " + testTimeout + " seconds");
return false;
}
int exitCode = process.exitValue();
if (exitCode != 0) {
- getLog().warn("TestBench test exited with code: " + exitCode);
+ getLog().warn("Test exited with code: " + exitCode);
return false;
}
return true;
} catch (Exception e) {
- throw new MojoExecutionException("Failed to run TestBench test", e);
+ throw new MojoExecutionException("Failed to run test", e);
}
}
}
diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RunMojo.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RunMojo.java
index a255a2017..d802a74af 100644
--- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RunMojo.java
+++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/K6RunMojo.java
@@ -44,13 +44,13 @@
*
*
* // Run tests sequentially
- * mvn k6:run -Dk6.testDir=k6/tests -Dk6.vus=50 -Dk6.duration=1m
+ * mvn loadtest:run -Dk6.testDir=k6/tests -Dk6.vus=50 -Dk6.duration=1m
*
* // Run tests in parallel with equal weights
- * mvn k6:run -Dk6.testDir=k6/tests -Dk6.combineScenarios=true -Dk6.vus=50
+ * mvn loadtest:run -Dk6.testDir=k6/tests -Dk6.combineScenarios=true -Dk6.vus=50
*
* // Run tests in parallel with custom weights (70% hello-world, 30% crud-example)
- * mvn k6:run -Dk6.testDir=k6/tests -Dk6.combineScenarios=true \
+ * mvn loadtest:run -Dk6.testDir=k6/tests -Dk6.combineScenarios=true \
* -Dk6.scenarioWeights="helloWorld:70,crudExample:30"
*
*/
@@ -225,9 +225,9 @@ private void reportActuatorMetrics() {
if (metrics.isPresent()) {
getLog().info("");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info(metrics.get().toString());
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
} else {
getLog().debug("Actuator metrics not available at " + appIp + ":"
+ managementPort);
@@ -241,7 +241,7 @@ private void runCombinedScenarios(List testFiles)
throws MojoExecutionException {
getLog().info("Running " + testFiles.size()
+ " scenarios in parallel (combined mode)");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info(" Total virtual users: " + virtualUsers);
getLog().info(" Duration: " + duration);
getLog().info(" Target: http://" + appIp + ":" + appPort);
@@ -258,7 +258,7 @@ private void runCombinedScenarios(List testFiles)
getLog().info(" Scenario: " + scenarioName + " (weight: " + weight
+ "%)");
}
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info("");
// Generate combined test file
@@ -280,9 +280,9 @@ private void runCombinedScenarios(List testFiles)
nodeRunner.runK6Test(combinedFile, virtualUsers, duration, appIp,
appPort, true);
getLog().info("");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info("Combined scenario test completed successfully");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
} catch (MojoExecutionException e) {
if (failOnThreshold) {
throw e;
@@ -300,7 +300,7 @@ private void runSequentialTests(List filesToRun)
throws MojoExecutionException {
getLog().info("Running " + filesToRun.size()
+ " k6 load test(s) sequentially");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info(" Virtual users: " + virtualUsers);
getLog().info(" Duration: " + duration);
getLog().info(" Target: http://" + appIp + ":" + appPort);
@@ -308,7 +308,7 @@ private void runSequentialTests(List filesToRun)
for (Path test : filesToRun) {
getLog().info(" - " + test.getFileName());
}
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info("");
int passed = 0;
@@ -338,10 +338,10 @@ private void runSequentialTests(List filesToRun)
}
getLog().info("");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
getLog().info("k6 load test summary: " + passed + " passed, " + failed
+ " failed");
- getLog().info("========================================");
+ getLog().info(CONTENT_BREAK);
}
/**
diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/PlaywrightRecordMojo.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/PlaywrightRecordMojo.java
new file mode 100644
index 000000000..bd8e1e373
--- /dev/null
+++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/PlaywrightRecordMojo.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (C) 2000-2026 Vaadin Ltd
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ * See for the full
+ * license.
+ */
+package com.vaadin.testbench.loadtest;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+
+/**
+ * Records Playwright Java tests and converts their HAR output to k6 load tests.
+ *
+ * Unlike the TestBench {@code record} goal, this does NOT use a recording
+ * proxy. Instead, it relies on Playwright's native HAR recording capability.
+ * The test class must extend {@code AbstractPlaywrightIT} (or otherwise
+ * configure {@code BrowserContext} with {@code setRecordHarPath}) to produce
+ * HAR output when the {@code k6.harOutputPath} system property is set.
+ *
+ * For each test class, this goal:
+ *
+ * - Runs the Playwright test with {@code -Dk6.harOutputPath=...}
+ * - Collects the HAR file produced by Playwright
+ * - Filters external domains from the HAR
+ * - Converts the HAR to a k6 script
+ * - Refactors the script for Vaadin compatibility
+ *
+ *
+ * Example usage:
+ *
+ *
+ * mvn k6:record-playwright -Dk6.testClasses=HelloWorldPlaywrightIT,CrudExamplePlaywrightIT
+ *
+ */
+@Mojo(name = "record-playwright", defaultPhase = LifecyclePhase.INTEGRATION_TEST)
+public class PlaywrightRecordMojo extends AbstractRecordMojo {
+
+ @Override
+ protected String getGoalName() {
+ return "record-playwright";
+ }
+
+ @Override
+ protected String getTestFrameworkName() {
+ return "Playwright";
+ }
+
+ @Override
+ protected void logRecordingConfiguration() {
+ getLog().info(" App port: " + appPort);
+ getLog().info(
+ " Using Playwright native HAR recording (no proxy needed)");
+ }
+
+ @Override
+ protected void recordHar(String currentTestClass, Path harPath)
+ throws MojoExecutionException, InterruptedException, IOException {
+ boolean testSuccess = runPlaywrightTest(currentTestClass, harPath);
+
+ if (!Files.exists(harPath)) {
+ throw new MojoExecutionException("HAR file was not created: "
+ + harPath
+ + ". Ensure the test class extends AbstractPlaywrightIT or "
+ + "configures BrowserContext with setRecordHarPath when "
+ + "k6.harOutputPath system property is set.");
+ }
+
+ long harSize = Files.size(harPath);
+ if (harSize < 100) {
+ throw new MojoExecutionException(
+ "HAR file appears empty (size: " + harSize + " bytes)");
+ }
+
+ getLog().info(
+ "HAR file created: " + harPath + " (" + harSize + " bytes)");
+
+ if (!testSuccess) {
+ getLog().warn(
+ "Playwright test may have failed, but HAR was recorded. Continuing with conversion...");
+ }
+ }
+
+ private boolean runPlaywrightTest(String currentTestClass,
+ Path harOutputPath) throws MojoExecutionException {
+ getLog().info("Running Playwright test: " + currentTestClass);
+
+ List command = buildBaseTestCommand(currentTestClass);
+ command.add("-Dk6.harOutputPath=" + harOutputPath.toAbsolutePath());
+
+ return runMavenTest(command);
+ }
+}
diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/TestbenchRecordMojo.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/TestbenchRecordMojo.java
new file mode 100644
index 000000000..71cb32637
--- /dev/null
+++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/TestbenchRecordMojo.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (C) 2000-2026 Vaadin Ltd
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ * See for the full
+ * license.
+ */
+package com.vaadin.testbench.loadtest;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Records TestBench tests through a proxy and converts them to k6 load tests.
+ *
+ * This goal supports recording multiple test classes. For each test class it:
+ *
+ * - Starts a recording proxy on the specified port
+ * - Runs the TestBench test class with proxy configuration
+ * - Stops the proxy and saves the HAR file
+ * - Converts the HAR to a k6 test (filters, converts, refactors)
+ *
+ *
+ * Example usage:
+ *
+ *
+ * mvn k6:record -Dk6.testClasses=HelloWorldIT,CrudExampleIT
+ *
+ */
+@Mojo(name = "record", defaultPhase = LifecyclePhase.INTEGRATION_TEST)
+public class TestbenchRecordMojo extends AbstractRecordMojo {
+
+ /**
+ * Port for the recording proxy.
+ */
+ @Parameter(property = "k6.proxyPort", defaultValue = "6000")
+ private int proxyPort;
+
+ @Override
+ protected String getGoalName() {
+ return "record";
+ }
+
+ @Override
+ protected String getTestFrameworkName() {
+ return "TestBench";
+ }
+
+ @Override
+ protected void logRecordingConfiguration() {
+ getLog().info(" Proxy port: " + proxyPort);
+ getLog().info(" App port: " + appPort);
+ }
+
+ @Override
+ protected void recordHar(String currentTestClass, Path harPath)
+ throws MojoExecutionException, InterruptedException, IOException {
+ try {
+ // Start proxy recorder
+ nodeRunner.startProxyRecorder(proxyPort, harPath);
+
+ // Run TestBench test
+ boolean testSuccess = runTestBenchTest(currentTestClass);
+
+ // Verify proxy captured traffic before stopping
+ int recordedEntries = nodeRunner.getRecordedEntryCount();
+ if (recordedEntries == 0) {
+ nodeRunner.stopProxyRecorder();
+ throw new MojoExecutionException(
+ "Proxy recorded 0 requests for test '"
+ + currentTestClass + "'. "
+ + "The test likely did not route traffic through the proxy on port "
+ + proxyPort + ". "
+ + "Verify that the K6RecordingExtension is on the classpath and "
+ + "junit.jupiter.extensions.autodetection.enabled=true is set.");
+ }
+ getLog().info("Proxy captured " + recordedEntries + " requests");
+
+ // Stop proxy (this writes the HAR file)
+ nodeRunner.stopProxyRecorder();
+
+ // Wait a moment for HAR file to be written
+ Thread.sleep(1000);
+
+ if (!Files.exists(harPath)) {
+ throw new MojoExecutionException(
+ "HAR file was not created: " + harPath);
+ }
+
+ getLog().info("HAR file created: " + harPath + " ("
+ + Files.size(harPath) + " bytes)");
+
+ if (!testSuccess) {
+ getLog().warn(
+ "TestBench test may have failed, but HAR was recorded. Continuing with conversion...");
+ }
+ } finally {
+ // Ensure proxy is stopped
+ nodeRunner.stopProxyRecorder();
+ }
+ }
+
+ private boolean runTestBenchTest(String currentTestClass)
+ throws MojoExecutionException {
+ getLog().info("Running TestBench test: " + currentTestClass);
+
+ List command = buildBaseTestCommand(currentTestClass);
+ command.add("-Dk6.proxy.host=localhost:" + proxyPort);
+ // Enable JUnit 5 auto-detection for K6RecordingExtension
+ command.add("-Djunit.jupiter.extensions.autodetection.enabled=true");
+
+ return runMavenTest(command);
+ }
+}