How would you design a Selenium-Java framework to accurately simulate diverse real-world network conditions and capture core Web Vitals (LCP, FID, CLS) for continuous performance monitoring in a CI/CD pipeline?
Question
How would you design a Selenium-Java framework to accurately simulate diverse real-world network conditions and capture core Web Vitals (LCP, FID, CLS) for continuous performance monitoring in a CI/CD pipeline?
Answer
Designing a Selenium-Java framework for accurate network simulation and Web Vitals capture requires leveraging advanced browser capabilities, typically through the Chrome DevTools Protocol (CDP), and integrating performance monitoring techniques into your test lifecycle. This approach elevates functional testing to include critical performance insights, crucial for modern web applications.
Architectural Components
- DevTools Protocol Integration: Selenium 4 natively supports CDP, which is essential for network emulation and performance metric extraction.
- Network Condition Profiles: Define various realistic network profiles (e.g., “Good 3G”, “Regular 4G”, “Fast WiFi”) with specific latency, throughput, and packet loss values.
- Web Vitals Measurement Utility: A dedicated component to inject and observe JavaScript code on the page for capturing LCP, FID, and CLS using the
PerformanceObserverAPI. - Data Storage & Reporting: A mechanism to store collected performance metrics (e.g., JSON files, a time-series database like InfluxDB) and generate reports or integrate with dashboards (e.g., Grafana).
- Test Listener/Interceptor: Hooks within the test framework (e.g., JUnit
TestWatcher, TestNGITestListener) to manage DevTools sessions, apply network conditions, capture metrics, and log data. - CI/CD Integration: Scripts to execute tests with performance profiling, collect results, and potentially trigger alerts based on defined thresholds.
Implementation Steps
1. Initializing WebDriver with DevTools
Ensure you are using Selenium 4+ and a Chromium-based browser (Chrome, Edge).
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.v125.network.Network; // Adjust version as per your Selenium release
import org.openqa.selenium.devtools.v125.network.model.ConnectionType; // Adjust version
import java.util.Optional;
public class PerformanceWebDriver {
private WebDriver driver;
private DevTools devTools;
public PerformanceWebDriver() {
// Ensure chromedriver is in PATH or specify its path
// System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
driver = new ChromeDriver();
devTools = ((ChromeDriver) driver).getDevTools();
devTools.createSession();
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
}
public WebDriver getDriver() {
return driver;
}
public DevTools getDevTools() {
return devTools;
}
public void quit() {
if (devTools != null) {
devTools.close();
}
if (driver != null) {
driver.quit();
}
}
}
2. Defining and Applying Network Conditions
Create an enum or class to define your network profiles. Use Network.emulateNetworkConditions via DevTools.
// NetworkProfile.java
public enum NetworkProfile {
OFFLINE(true, 0, 0, 0, ConnectionType.NONE),
GOOD_3G(false, 1500, 100 * 1024, 50 * 1024, ConnectionType.CELLULAR3G), // 1.5s latency, 100kbps down, 50kbps up
REGULAR_4G(false, 300, 1_500 * 1024, 750 * 1024, ConnectionType.CELLULAR4G), // 300ms latency, 1.5Mbps down, 750Kbps up
FAST_WIFI(false, 50, 10 * 1024 * 1024, 5 * 1024 * 1024, ConnectionType.WIFI); // 50ms latency, 10Mbps down, 5Mbps up
public final boolean offline;
public final long latency; // ms
public final long downloadThroughput; // bytes/sec
public final long uploadThroughput; // bytes/sec
public final ConnectionType connectionType;
NetworkProfile(boolean offline, long latency, long downloadThroughput, long uploadThroughput, ConnectionType connectionType) {
this.offline = offline;
this.latency = latency;
this.downloadThroughput = downloadThroughput;
this.uploadThroughput = uploadThroughput;
this.connectionType = connectionType;
}
}
// In your test or setup method, you would apply the condition:
public class NetworkConditionManager {
public void applyNetworkCondition(DevTools devTools, NetworkProfile profile) {
devTools.send(Network.emulateNetworkConditions(
profile.offline,
profile.latency,
profile.downloadThroughput,
profile.uploadThroughput,
Optional.of(profile.connectionType)
));
System.out.println("Applied network profile: " + profile.name());
}
public void resetNetworkCondition(DevTools devTools) {
devTools.send(Network.emulateNetworkConditions(
false, 0, 0, 0, Optional.of(ConnectionType.NONE) // Reset to no throttling
));
System.out.println("Network conditions reset.");
}
}
3. Capturing Core Web Vitals (LCP, FID, CLS)
This involves injecting a JavaScript PerformanceObserver that collects these metrics and makes them available for retrieval. Since FID requires user interaction, it might not be captured in purely automated page loads without simulating a click/scroll first. LCP and CLS are more readily available.
import org.openqa.selenium.JavascriptExecutor;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; // For explicit wait
public class WebVitalsMonitor {
// JavaScript to inject a PerformanceObserver and store latest metrics
// This script should be robust enough to be re-injected if the page navigates
private static final String WEB_VITALS_JS_SETUP = """
(function() {
// Check if performance observer is already set up to avoid duplicates on SPA navigations
if (window.webVitalsObserverSetup) {
return;
}
window.webVitalsObserverSetup = true;
window.webVitals = { lcp: null, fid: null, cls: null, ttfb: null, fcp: null };
const po = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (entry.name === 'LCP' && entry.startTime > (window.webVitals.lcp || 0)) {
window.webVitals.lcp = entry.startTime;
} else if (entry.name === 'FID' && entry.duration > (window.webVitals.fid || 0)) {
window.webVitals.fid = entry.duration;
} else if (entry.name === 'CLS') {
// CLS reports cumulative value, so we just take the latest
window.webVitals.cls = entry.value;
}
// Capture FCP
if (entry.name === 'first-contentful-paint' && entry.startTime > (window.webVitals.fcp || 0)) {
window.webVitals.fcp = entry.startTime;
}
}
});
po.observe({ type: 'largest-contentful-paint', buffered: true });
po.observe({ type: 'first-input-delay', buffered: true });
po.observe({ type: 'layout-shift', buffered: true });
po.observe({ type: 'paint', buffered: true }); // For FCP
// Get TTFB using Navigation Timing API
if (performance.timing) {
window.webVitals.ttfb = performance.timing.responseStart - performance.timing.navigationStart;
}
})();
""";
// JavaScript to retrieve the latest collected Web Vitals
private static final String WEB_VITALS_JS_RETRIEVE = """
return window.webVitals;
""";
public void startMonitoring(WebDriver driver) {
((JavascriptExecutor) driver).executeScript(WEB_VITALS_JS_SETUP);
System.out.println("Web Vitals monitoring initialized.");
}
public Map<String, Object> getWebVitals(WebDriver driver) {
// Give some time for metrics to be collected, especially for LCP/CLS
// In a real scenario, you'd wait for page load complete and then potentially
// wait a few more milliseconds or trigger interactions.
try {
TimeUnit.MILLISECONDS.sleep(1000); // Wait for observer to potentially fire and metrics to stabilize
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Web Vitals retrieval interrupted: " + e.getMessage());
}
// Execute the script and cast the result
Object result = ((JavascriptExecutor) driver).executeScript(WEB_VITALS_JS_RETRIEVE);
if (result instanceof Map) {
return (Map<String, Object>) result;
}
System.err.println("Failed to retrieve Web Vitals. Expected Map, got: " + (result != null ? result.getClass().getName() : "null"));
return new HashMap<>(); // Return empty map on failure
}
}
4. Integrating into Test Flow and Reporting
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By; // Example for interactions
import org.openqa.selenium.support.ui.WebDriverWait; // For explicit waits
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
import java.util.Map;
public class PerformanceTestExample {
private static PerformanceWebDriver performanceDriver;
private WebVitalsMonitor webVitalsMonitor;
private NetworkConditionManager networkManager;
@BeforeAll
static void setupDriver() {
performanceDriver = new PerformanceWebDriver();
}
@BeforeEach
void setupTest() {
webVitalsMonitor = new WebVitalsMonitor();
networkManager = new NetworkConditionManager();
}
@AfterEach
void tearDownTest() {
// Reset network conditions after each test to ensure isolation
networkManager.resetNetworkCondition(performanceDriver.getDevTools());
// Here you might also log/save results for the current test run
}
@AfterAll
static void tearDownDriver() {
if (performanceDriver != null) {
performanceDriver.quit();
}
}
@Test
@DisplayName("Load homepage under Good 3G and capture Web Vitals")
void testHomepageLoadGood3G() {
WebDriver driver = performanceDriver.getDriver();
networkManager.applyNetworkCondition(performanceDriver.getDevTools(), NetworkProfile.GOOD_3G);
driver.get("https://www.google.com"); // Navigate to a suitable URL for testing
webVitalsMonitor.startMonitoring(driver); // Start monitoring AFTER navigation or on page load events
// Wait for page to be fully loaded and interactive elements present
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.presenceOfElementLocated(By.name("q")));
// Optional: Perform a user interaction to trigger FID
// driver.findElement(By.name("q")).sendKeys("performance testing");
Map<String, Object> webVitals = webVitalsMonitor.getWebVitals(driver);
System.out.println("Web Vitals under Good 3G: " + webVitals);
// Assertions for performance thresholds
// Note: Returned values can be Long (integer) or Double (decimal) depending on JS engine & metric
if (webVitals.get("lcp") instanceof Number) {
Assertions.assertTrue(((Number) webVitals.get("lcp")).doubleValue() < 4000.0, "LCP under Good 3G exceeded 4 seconds");
}
// ... similar assertions for FID, CLS, TTFB, FCP
}
@Test
@DisplayName("Load homepage under Fast WiFi and capture Web Vitals")
void testHomepageLoadFastWiFi() {
WebDriver driver = performanceDriver.getDriver();
networkManager.applyNetworkCondition(performanceDriver.getDevTools(), NetworkProfile.FAST_WIFI);
driver.get("https://www.google.com"); // Navigate to a suitable URL for testing
webVitalsMonitor.startMonitoring(driver);
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.presenceOfElementLocated(By.name("q")));
Map<String, Object> webVitals = webVitalsMonitor.getWebVitals(driver);
System.out.println("Web Vitals under Fast WiFi: " + webVitals);
if (webVitals.get("lcp") instanceof Number) {
Assertions.assertTrue(((Number) webVitals.get("lcp")).doubleValue() < 1500.0, "LCP under Fast WiFi exceeded 1.5 seconds");
}
}
}
Advanced Considerations and CI/CD Integration
- Baseline Management: Establish baseline performance metrics for critical user journeys under various network conditions. This involves running tests against a stable environment and storing the results.
- Thresholds and Alerting: Define acceptable thresholds for LCP, FID, CLS, FCP, TTFB. Integrate with CI/CD tools (e.g., Jenkins, GitLab CI, GitHub Actions) to fail builds or send alerts (e.g., Slack, email) if these thresholds are consistently breached, indicating a performance regression.
- Data Aggregation and Persistence: Store collected metrics in a persistent and queryable format. Time-series databases like InfluxDB are ideal for performance data. For simpler setups, JSON or CSV files can work, processed by custom scripts.
- Reporting and Visualization: Integrate the stored data with visualization tools like Grafana, Kibana, or custom dashboards. This allows for historical trend analysis, comparison across builds, and easy identification of performance degradation or improvements.
- Performance Budgeting: Implement performance budgets in your CI pipeline. If a code change causes the page weight or crucial Web Vitals to exceed predefined limits, the build should fail. Tools like Lighthouse CI can complement Selenium for more comprehensive auditing.
- Scalability and Parallelism: When running large suites, consider using Selenium Grid or cloud-based execution platforms (e.g., BrowserStack, Sauce Labs, LambdaTest) that support DevTools integration. Ensure your framework is designed to run tests in parallel to optimize execution time without introducing contention for metrics.
- Variability Handling: Performance tests can be inherently flaky due to external factors. Implement retry mechanisms for failing tests, run tests multiple times for each scenario, and analyze median or 90th percentile values rather than single-run averages to get a more robust picture.
- Resource Cleanup: Crucially, ensure DevTools sessions and browser instances are properly closed after each test or test suite to prevent resource leaks and ensure stability in a continuous integration environment. This is handled by the
quit()method inPerformanceWebDriver. - Monitoring SPA Navigations: For Single Page Applications (SPAs), Web Vitals might reset or change on client-side navigations. The
PerformanceObserverscript needs to be robustly handled, potentially re-initializing or adapting to new route loads without full page refreshes.
By integrating these components, your Selenium-Java framework transforms into a powerful tool for continuous performance monitoring, enabling proactive identification of regressions and ensuring an optimal user experience across various network conditions.
📲 Practice Offline on Mobile: Download the free QA Automation & SDET Prep app on Google Play & App Store.