Integrating Google Lighthouse with Playwright; Picture this: Your development team just shipped a major feature update. The code passed all functional tests. QA signed off. Everything looks perfect in staging. You hit deploy with confidence.
Then the complaints start rolling in.
“The page takes forever to load.” “Images are broken on mobile.” “My browser is lagging.”
Sound familiar? According to Google, 53% of mobile users abandon sites that take longer than 3 seconds to load. Yet most teams only discover performance issues after they’ve reached production, when the damage to user experience and brand reputation is already done.
The real problem isn’t that teams don’t care about performance. It’s that performance testing is often manual, inconsistent, and disconnected from the development workflow. Performance degradation is gradual. It sneaks up on you. And by the time you notice, you’re playing catch-up instead of staying ahead.
The Gap Between Awareness and Action
Most engineering teams know they should monitor web performance. They’ve heard about Core Web Vitals, Time to Interactive, and First Contentful Paint. They understand that performance impacts SEO rankings, conversion rates, and user satisfaction.
But knowing and doing are two different things.
The challenge lies in making performance testing continuous, automated, and actionable. Manual audits are time-consuming and prone to human error. They create bottlenecks in the release pipeline. What teams need is a way to bake performance testing directly into their automation frameworks to treat performance as a first-class citizen alongside functional testing.
Enter Google Lighthouse.
What Is Google Lighthouse?
Google Lighthouse is an open-source, automated tool designed to improve the quality of web pages. Originally developed by Google’s Chrome team, Lighthouse has become the industry standard for web performance auditing by Integrating Google Lighthouse with Playwright.
But here’s what makes Lighthouse truly powerful: it doesn’t just measure performance it provides actionable insights.
When you run a Lighthouse audit, you get comprehensive scores across five key categories:
Performance: Load times, rendering metrics, and resource optimization
Accessibility: ARIA attributes, color contrast, semantic HTML
Best Practices: Security, modern web standards, browser compatibility
SEO: Meta tags, mobile-friendliness, structured data
Progressive Web App: Service workers, offline functionality, installability
Each category receives a score from 0 to 100, with detailed breakdowns of what’s working and what needs improvement. The tool analyzes critical metrics like:
First Contentful Paint (FCP): When the first content renders
Largest Contentful Paint (LCP): When the main content is visible
Total Blocking Time (TBT): How long the page is unresponsive
Cumulative Layout Shift (CLS): Visual stability during load
Speed Index: How quickly content is visually populated
These metrics align directly with Google’s Core Web Vitals the signals that impact search rankings and user experience.
Why Performance Can’t Be an Afterthought
Let’s talk numbers, because performance isn’t just a technical concern it’s a business imperative.
Amazon found that every 100ms of latency cost them 1% in sales. Pinterest increased sign-ups by 15% after reducing perceived wait time by 40%. The BBC discovered they lost an additional 10% of users for every extra second their site took to load.
The data is clear: performance directly impacts your bottom line.
But beyond revenue, there’s the SEO factor. Since 2021, Google has used Core Web Vitals as ranking signals. Sites with poor performance scores get pushed down in search results. You could have the most comprehensive content in your niche, but if your LCP is above 4 seconds, you’re losing visibility.
The question isn’t whether performance matters. The question is: how do you ensure performance doesn’t degrade as your application evolves?
The Power of Integration: Lighthouse Meets Automation
This is where the magic happens when you integrate Google Lighthouse into your automation frameworks.
By Integrating Google Lighthouse with Playwright, Selenium, or Cypress, you transform performance from a periodic manual check into a continuous, automated quality gate.
Here’s what this integration delivers:
1. Consistency Across Environments
Automated Lighthouse tests run in controlled environments with consistent configurations, giving you reliable, comparable data across test runs.
2. Early Detection of Performance Regressions
Instead of discovering performance issues in production, you catch them during development. A developer adds a large unoptimized image? The Lighthouse test fails before the code merges.
3. Performance Budgets and Thresholds
You can set specific performance budgets for example, “Performance score must be above 90.” If a change violates these budgets, the build fails, just like a failing functional test.
4. Comprehensive Reporting
Lighthouse generates detailed HTML and JSON reports with visual breakdowns, diagnostic information, and specific recommendations. These reports become part of your test artifacts.
How Integration Works: A High-Level Flow
You don’t need to be a performance expert to integrate Lighthouse into your automation framework. The process is straightforward and fits naturally into existing testing workflows.
Step 1: Install Lighthouse Lighthouse is available as an npm package, making it easy to add to any Node.js-based automation project. It integrates seamlessly with popular frameworks.
Step 2: Configure Your Audits Define what you want to test which pages, which metrics, and what thresholds constitute a pass or fail. You can customize Lighthouse to focus on specific categories or run full audits across all five areas.
Step 3: Integrate with Your Test Suite Add Lighthouse audits to your existing test files. Your automation framework handles navigation and setup, then hands off to Lighthouse for the performance audit. The results come back as structured data you can assert against.
Step 4: Set Performance Budgets Define acceptable thresholds for key metrics. These become your quality gates if performance drops below the threshold, the test fails and the pipeline stops.
Step 5: Generate and Store Reports Configure Lighthouse to generate HTML and JSON reports. Store these as test artifacts in your CI/CD system, making them accessible for review and historical analysis.
Step 6: Integrate with CI/CD Run Lighthouse tests as part of your continuous integration pipeline. Every pull request, every deployment performance gets validated automatically.
The beauty of this approach is that it requires minimal changes to your existing workflow. You’re not replacing your automation framework you’re enhancing it with performance capabilities.
Practical Implementation: Code Examples
Let’s look at how this works in practice with a real Playwright automation framework. Here’s how you can create a reusable Lighthouse runner:
Feature: Integrating Google Lighthouse with the Test Automation Framework
This feature leverages Google Lighthouse to evaluate the performance,
accessibility, SEO, and best practices of web pages.
@test
Scenario: Validate the Lighthouse Performance Score for the Playwright Official Page
Given I navigate to the Playwright official website
When I initiate the Lighthouse audit
And I click on the "Get started" button
And I wait for the Lighthouse report to be generated
Then I generate the Lighthouse report
Decoding Lighthouse Reports: What the Data Tells You
Lighthouse reports are information-rich, but they’re designed to be actionable, not overwhelming. Let’s break down what you get:
The Performance Score
This is your headline number a weighted average of key performance metrics. A score of 90-100 is excellent, 50-89 needs improvement, and below 50 requires immediate attention.
Metric Breakdown
Each performance metric gets its own score and timing. You’ll see exactly how long FCP, LCP, TBT, CLS, and Speed Index took, color-coded to show if they’re in the green, orange, or red zone.
Opportunities
This section is gold. Lighthouse identifies specific optimizations that would improve performance, ranked by potential impact. “Eliminate render-blocking resources” might save 2.5 seconds. “Properly size images” could save 1.8 seconds. Each opportunity includes technical details and implementation guidance.
Diagnostics
These are additional insights that don’t directly impact the performance score but highlight areas for improvement things like excessive DOM size, unused JavaScript, or inefficient cache policies.
Passed Audits
Don’t ignore these! They show what you’re doing right, which is valuable for understanding your performance baseline and maintaining good practices.
Accessibility and SEO Insights
Beyond performance, you get actionable feedback on accessibility issues (missing alt text, poor color contrast) and SEO problems (missing meta descriptions, unreadable font sizes on mobile).
The JSON output is equally valuable for programmatic analysis. You can extract specific metrics, track them over time, and build custom dashboards or alerts based on performance trends.
Real-World Impact
Let’s look at practical scenarios where this integration delivers measurable value:
E-Commerce Platform
An online retailer integrated Lighthouse into their Playwright test suite, running audits on product pages and checkout flows. They set a performance budget requiring scores above 90. Within three months, they caught 14 performance regressions before production, including a third-party analytics script blocking rendering.
A B2B SaaS company added Lighthouse audits to their test suite, focusing on dashboard interfaces. They discovered their data visualization library was causing significant Total Blocking Time. The Lighthouse diagnostics pointed them to specific JavaScript bundles needing code-splitting.
Result: Reduced TBT by 60%, improving perceived responsiveness and reducing support tickets.
Content Publisher
A media company integrated Lighthouse into their deployment pipeline, auditing article pages with strict accessibility and SEO thresholds. This caught issues like missing alt text, poor heading hierarchy, and oversized media files.
Result: Improved SEO rankings, increased organic traffic by 23%, and ensured WCAG compliance.
The Competitive Advantage
Here’s what separates high-performing teams from the rest: they treat performance as a feature, not an afterthought.
By integrating Google Lighthouse with Playwright or any other automation framework, you’re building a culture of performance awareness. Developers get immediate feedback on the performance impact of their changes. Stakeholders get clear, visual reports demonstrating the business value of optimization work.
You shift from reactive firefighting to proactive prevention. Instead of scrambling to fix performance issues after users complain, you prevent them from ever reaching production.
Getting Started
You don’t need to overhaul your entire testing infrastructure. Start small:
Pick one critical user journey maybe your homepage or checkout flow
Add a single Lighthouse audit to your existing test suite
Set a baseline by running the audit and recording current scores
Define one performance budget perhaps a performance score above 80
Integrate it into your CI/CD pipeline so it runs automatically
From there, you can expand add more pages, tighten thresholds, incorporate additional metrics. The key is to start building that performance feedback loop.
Conclusion: Performance as a Continuous Practice
Integrating Google Lighthouse with Playwright; Web performance isn’t a one-time fix. It’s an ongoing commitment that requires visibility, consistency, and automation. Google Lighthouse provides the measurement and insights. Your automation framework provides the execution and integration. Together, they create a powerful system for maintaining and improving web performance at scale.
The teams that win in today’s digital landscape are those that make performance testing as routine as functional testing. They’re the ones catching regressions early, maintaining high standards, and delivering consistently fast experiences to their users.
The question is: will you be one of them?
Would you be ready to boost your web performance? You can start by integrating Google Lighthouse into your automation framework today. Your users and your bottom line will thank you.
API Automation Testing Framework – In Today’s fast-paced digital ecosystem, almost every modern application relies on APIs (Application Programming Interfaces) to function seamlessly. Whether it’s a social media integration pulling live updates, a payment gateway processing transaction, or a data service exchanging real-time information, APIs act as the invisible backbone that connects various systems together.
Because APIs serve as the foundation of all interconnected software, ensuring that they are reliable, secure, and high performing is absolutely critical. Even a minor API failure can impact multiple dependent systems; consequently, it may cause application downtime, data mismatches, or even financial loss.
That’s where API automation testing framework comes in. Unlike traditional UI testing, API testing validates the core business logic directly at the backend layer, which makes it faster, more stable, and capable of detecting issues early in the development cycle — even before the frontend is ready.
In this blog, we’ll walk through the process of building a complete API Automation Testing Framework using a combination of:
Java – as the main programming language
Maven – for project and dependency management
Cucumber – to implement Behavior Driven Development (BDD)
RestAssured – for simplifying RESTful API automation
Playwright – to handle browser-based token generation
The framework you’ll learn to build will follow a BDD (Behavior-Driven Development) approach, enabling test scenarios to be written in simple, human-readable language. This not only improves collaboration between developers, testers, and business analysts but also makes test cases easier to understand, maintain, and extend.
Additionally, the API automation testing framework will be CI/CD-friendly, meaning it can be seamlessly integrated into automated build pipelines for continuous testing and faster feedback.
By the end of this guide, you’ll have a scalable, reusable, and maintainable API testing framework that brings together the best of automation, reporting, and real-time token management — a complete solution for modern QA teams.
What is API?
An API (Application Programming Interface) acts as a communication bridge between two software systems, allowing them to exchange information in a standardized way. In simpler terms, it defines how different software components should interact — through a set of rules, protocols, and endpoints.
Think of an API as a messenger that takes a request from one system, delivers it to another system, and then brings back the response. This interaction, therefore, allows applications to share data and functionality without exposing their internal logic or database structure.
Let’s take a simple example: When you open a weather application on your phone, it doesn’t store weather data itself. Instead, it sends a request to a weather server API, which processes the request and sends back a response — such as the current temperature, humidity, or forecast. This request-response cycle is what makes APIs so powerful and integral to almost every digital experience we use today.
Most modern APIs follow the REST (Representational State Transfer) architectural style. REST APIs use the HTTP protocol and are designed around a set of standardized operations, including:
HTTP Method
Description
Example Use
GET
Retrieve data from the server
Fetch a list of users
POST
Create new data on the server
Add a new product
PUT
Update existing data
edit user details
DELETE
Remove data
Delete a record
The responses returned by API’s are typically in JSON (JavaScript Object Notation) format – a lightweight, human-readable, and machine-friendly data format that’s easy to parse and validate.
In essence, API’s are the digital glue that holds modern applications together — enabling smooth communication, faster integrations, and a consistent flow of information across systems.
What is API Testing?
API Testing is the process of verifying that an API functions correctly and performs as expected — ensuring that all its endpoints, parameters, and data exchanges behave according to defined business rules.
In simple terms, it’s about checking whether the backend logic of an application works properly — without needing a graphical user interface (UI). Since APIs act as the communication layer between different software components, testing them helps ensure that the entire system remains reliable, secure, and efficient.
API testing typically focuses on four main aspects:
Functionality – Does the API perform the intended operation and return the correct response for valid requests?
Reliability – Does it deliver consistent results every time, even under different inputs and conditions?
Security – Is the API protected from unauthorized access, data leaks, or token misuse?
Performance – Does it respond quickly and remain stable under heavy load or high traffic?
Unlike traditional UI testing, which validates the visual and interactive parts of an application, API testing operates directly at the business logic layer. This makes it:
Faster – Since it bypasses the UI, execution times are much shorter.
More Stable – UI changes (like a button name or layout) don’t affect API tests.
Proactive – Tests can be created and run even before the front-end is developed.
In essence, API testing ensures the heart of your application is healthy. By validating responses, performance, and security at the API level, teams can detect defects early, reduce costs, and deliver more reliable software to users.
Why is API Testing Important?
API Testing plays a vital role in modern software development because APIs form the backbone of most applications. A failure in an API can affect multiple systems and impact overall functionality.
Here’s why API testing is important:
Ensures Functionality: Verifies that endpoints return correct responses and handle errors properly.
Enhances Security: Detects vulnerabilities like unauthorized access or token misuse.
Validates Data Integrity: Confirms that data remains consistent across APIs and databases.
Improves Performance: Checks response time, stability, and behavior under load.
Detects Defects Early: Allows early testing right after backend development, saving time and cost
Supports Continuous Integration: Easily integrates with CI/CD pipelines for automated validation.
In short, API testing ensures your system’s core logic is reliable, secure, and ready for real-world use.
Tools for Manual API Testing
Before jumping into automation, it’s essential to explore and understand APIs manually. Manual testing helps you validate endpoints, check responses, and get familiar with request structures.
Here are some popular tools used for manual API testing:
Postman: The most widely used tool for sending API requests, validating responses, and organizing test collections [refer link – https://www.postman.com/.
SoapUI: Best suited for testing both SOAP and REST APIs with advanced features like assertions and mock services.
Insomnia: A lightweight and user-friendly alternative to Postman, ideal for quick API exploration.
cURL: A command-line tool perfect for making fast API calls or testing from scripts.
Fiddler: Excellent for capturing and debugging HTTP/HTTPS traffic between client and server.
Using these tools helps testers understand API behavior, request/response formats, and possible edge cases — forming a strong foundation before moving to API automation.
Tools for API Automation Testing
After verifying APIs manually, the next step is to automate them using reliable tools and libraries. Automation helps improve test coverage, consistency, and execution speed.
Here are some popular tools used for API automation testing:
RestAssured: A powerful Java library designed specifically for testing and validating RESTful APIs.
Cucumber: Enables writing test cases in Gherkin syntax (plain English), making them easy to read and maintain.
Playwright: Automates browser interactions; in our framework, it will be used for token generation or authentication flows.
Postman + Newman: Allows you to run Postman collections directly from the command line — ideal for CI/CD integration.
JMeter: A robust tool for performance and load testing of APIs under different conditions.
In this blog, our focus will be on building a framework using RestAssured, Cucumber, and Playwright — combining functional, BDD, and authentication automation into one cohesive setup.
Framework Overview
We’ll build a Behavior-Driven API Automation Testing Framework that combines multiple tools for a complete testing solution. Here’s how each component fits in:
Cucumber – Manages the BDD layer, allowing test scenarios to be written in simple, readable feature files.
RestAssured – Handles HTTP requests and responses for validating RESTful APIs.
Playwright – Automates browser-based actions like token generation or authentication.
Maven – Manages project dependencies, builds, and plugins efficiently.
Cucumber HTML Reports – Automatically generates detailed execution reports after each run.
The framework follows a modular structure, with separate packages for step definitions, utilities, configurations, and feature files — ensuring clean organization, easy maintenance, and scalability.
In this, we will be creating a feature file for API Automation Testing Framework. A feature file consists of steps. These steps are mentioned in the gherkin language. The feature is easy to understand and can be written in the English language so that a non-technical person can understand the flow of the test scenario. In this framework we will be automating the four basic API request methods i.e. POST, PUT, GET and DELETE.
We can assign tags to our scenarios mentioned in the feature file to run particular test scenarios based on the requirement. The key point you must notice here is the feature file should end with .feature extension. We will be creating four different scenarios for the four different API methods.
Feature: All Notes API Validation
@api
Scenario Outline: Validate POST Create Notes API Response for "<scenarioName>" Scenario
When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>"
Then User verifies the response status code is <statusCode>
And User verifies the response body matches JSON schema "<schemaFile>"
Then User verifies fields in response: "<contentType>" with content type "<fields>"
Examples:
| scenarioName | method | url | headers | queryFile | bodyFile | statusCode | schemaFile | contentType | fields |
| Valid create Notes | POST | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes | NA | NA | Create_Notes_Request | 200 | NA | NA | NA |
Scenario Outline: Validate GET Notes API Response for "<scenarioName>" Scenario
When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>"
Then User verifies the response status code is <statusCode>
And User verifies the response body matches JSON schema "<schemaFile>"
Then User verifies fields in response: "<contentType>" with content type "<fields>"
Examples:
| scenarioName | method | url | headers | queryFile | bodyFile | statusCode | schemaFile | contentType | fields |
| Valid Get Notes | GET | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes | NA | NA | NA | 200 | Notes_Schema_200 | json | note=This is Note 1 |
Scenario Outline: Validate Update Notes API Response for "<scenarioName>" Scenario
When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>"
Then User verifies the response status code is <statusCode>
And User verifies the response body matches JSON schema "<schemaFile>"
Then User verifies fields in response: "<contentType>" with content type "<fields>"
Examples:
| scenarioName | method | url | headers | queryFile | bodyFile | statusCode | schemaFile | contentType | fields |
| Valid update Notes | PUT | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes/{noteId}/update-notes | NA | NA | Update_Notes_Request | 200 | NA | NA | NA |
Scenario Outline: Validate DELETE Create Notes API Response for "<scenarioName>" Scenario
When User sends "<method>" request to "<url>" with headers "<headers>" and query file "<queryFile>" and requestDataFile "<bodyFile>"
Then User verifies the response status code is <statusCode>
And User verifies the response body matches JSON schema "<schemaFile>"
Then User verifies fields in response: "<contentType>" with content type "<fields>"
Examples:
| scenarioName | method | url | headers | queryFile | bodyFile | statusCode | schemaFile | contentType | fields |
| Valid delete | DELETE | /api/v1/loan-syndications/{dealId}/investors/{investorId}/notes/{noteId} | NA | NA | NA | 200 | NA | NA | NA |
Step 4: Creating a Step Definition File
Unlike the automation framework which we have built in the previous blog, we will be creating a single-step file for all the feature files. In the BDD framework, the step files are used to map and implement the steps described in the feature file. Rest Assured library is very accurate to map the steps with the steps described in the feature file. We will be describing the same steps in the step file as they have described in the feature file so that behave will come to know the step implementation for the particular steps present in the feature file.
package org.Spurqlabs.Steps;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import io.restassured.response.Response;
import org.Spurqlabs.Core.TestContext;
import org.Spurqlabs.Utils.*;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import static org.Spurqlabs.Utils.DealDetailsManager.replacePlaceholders;
import static org.hamcrest.Matchers.equalTo;
public class CommonSteps extends TestContext {
private Response response;
@When("User sends {string} request to {string} with headers {string} and query file {string} and requestDataFile {string}")
public void user_sends_request_to_with_query_file_and_requestDataFile (String method, String url, String headers, String queryFile, String bodyFile) throws IOException {
String jsonString = Files.readString(Paths.get(FrameworkConfigReader.getFrameworkConfig("DealDetails")), StandardCharsets.UTF_8);
JSONObject storedValues = new JSONObject(jsonString);
String fullUrl = FrameworkConfigReader.getFrameworkConfig("BaseUrl") + replacePlaceholders(url);
Map<String, String> header = new HashMap<>();
if (!"NA".equalsIgnoreCase(headers)) {
header = JsonFileReader.getHeadersFromJson(FrameworkConfigReader.getFrameworkConfig("headers") + headers + ".json");
} else {
header.put("cookie", TokenManager.getToken());
}
Map<String, String> queryParams = new HashMap<>();
if (!"NA".equalsIgnoreCase(queryFile)) {
queryParams = JsonFileReader.getQueryParamsFromJson(FrameworkConfigReader.getFrameworkConfig("Query_Parameters") + queryFile + ".json");
for (String key : queryParams.keySet()) {
String value = queryParams.get(key);
for (String storedKey : storedValues.keySet()) {
value = value.replace("{" + storedKey + "}", storedValues.getString(storedKey));
}
queryParams.put(key, value);
}
}
Object requestBody = null;
if (!"NA".equalsIgnoreCase(bodyFile)) {
String bodyTemplate = JsonFileReader.getJsonAsString(
FrameworkConfigReader.getFrameworkConfig("Request_Bodies") + bodyFile + ".json");
for (String key : storedValues.keySet()) {
String placeholder = "{" + key + "}";
if (bodyTemplate.contains(placeholder)) {
bodyTemplate = bodyTemplate.replace(placeholder, storedValues.getString(key));
}
}
requestBody = bodyTemplate;
}
response = APIUtility.sendRequest(method, fullUrl, header, queryParams, requestBody);
response.prettyPrint();
TestContextLogger.scenarioLog("API", "Request sent: " + method + " " + fullUrl);
if (scenarioName.contains("GET Notes") && response.getStatusCode() == 200) {
DealDetailsManager.put("noteId", response.path("[0].id"));
}
}
@Then("User verifies the response status code is {int}")
public void userVerifiesTheResponseStatusCodeIsStatusCode(int statusCode) {
response.then().statusCode(statusCode);
TestContextLogger.scenarioLog("API", "Response status code: " + statusCode);
}
@Then("User verifies the response body matches JSON schema {string}")
public void userVerifiesTheResponseBodyMatchesJSONSchema(String schemaFile) {
if (!"NA".equalsIgnoreCase(schemaFile)) {
String schemaPath = "Schema/" + schemaFile + ".json";
response.then().assertThat().body(matchesJsonSchemaInClasspath(schemaPath));
TestContextLogger.scenarioLog("API", "Response body matches schema");
} else {
TestContextLogger.scenarioLog("API", "Response body does not have schema to validate");
}
}
@Then("User verifies field {string} has value {string}")
public void userVerifiesFieldHasValue(String jsonPath, String expectedValue) {
response.then().body(jsonPath, equalTo(expectedValue));
TestContextLogger.scenarioLog("API", "Field " + jsonPath + " has value: " + expectedValue);
}
@Then("User verifies fields in response: {string} with content type {string}")
public void userVerifiesFieldsInResponseWithContentType(String contentType, String fields) throws IOException {
// If NA, skip verification
if ("NA".equalsIgnoreCase(contentType) || "NA".equalsIgnoreCase(fields)) {
return;
}
String responseStr = response.getBody().asString().trim();
try {
if ("text".equalsIgnoreCase(contentType)) {
// For text, verify each expected value is present in response
for (String expected : fields.split(";")) {
expected = replacePlaceholders(expected.trim());
if (!responseStr.contains(expected)) {
throw new AssertionError("Expected text not found: " + expected);
}
TestContextLogger.scenarioLog("API", "Text found: " + expected);
}
} else if ("json".equalsIgnoreCase(contentType)) {
// For json, verify key=value pairs
JSONObject jsonResponse;
if (responseStr.startsWith("[")) {
JSONArray arr = new JSONArray(responseStr);
jsonResponse = !arr.isEmpty() ? arr.getJSONObject(0) : new JSONObject();
} else {
jsonResponse = new JSONObject(responseStr);
}
for (String pair : fields.split(";")) {
if (pair.trim().isEmpty()) continue;
String[] kv = pair.split("=", 2);
if (kv.length < 2) continue;
String keyPath = kv[0].trim();
String expected = replacePlaceholders(kv[1].trim());
Object actual = JsonFileReader.getJsonValueByPath(jsonResponse, keyPath);
if (actual == null) {
throw new AssertionError("Key not found in JSON: " + keyPath);
}
if (!String.valueOf(actual).equals(String.valueOf(expected))) {
throw new AssertionError("Mismatch for " + keyPath + ": expected '" + expected + "', got '" + actual + "'");
}
TestContextLogger.scenarioLog("API", "Validated: " + keyPath + " = " + expected);
}
} else {
throw new AssertionError("Unsupported content type: " + contentType);
}
} catch (AssertionError | Exception e) {
TestContextLogger.scenarioLog("API", "Validation failed: " + e.getMessage());
throw e;
}
}
Step 5: Creating API
Till now we have successfully created a feature file and a step file now in this step we will be creating a utility file. Generally, in Web automation, we have page files that contain the locators and the actions to perform on the web elements but in this framework, we will be creating a single utility file just like the step file. The utility file contains the API methods and the endpoints to perform the specific action like, POST, PUT, GET, or DELETE. The request body i.e. payload and the response body will be captured using the methods present in the utility file. So the reason these methods are created in the utility file is that we can use them multiple times and don’t have to create the same method over and over again.
package org.Spurqlabs.Utils;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import java.io.File;
import java.util.Map;
public class APIUtility {
public static Response sendRequest(String method, String url, Map<String, String> headers, Map<String, String> queryParams, Object body) {
RequestSpecification request = RestAssured.given();
if (headers != null && !headers.isEmpty()) {
request.headers(headers);
}
if (queryParams != null && !queryParams.isEmpty()) {
request.queryParams(queryParams);
}
if (body != null && !method.equalsIgnoreCase("GET")) {
if (headers == null || !headers.containsKey("Content-Type")) {
request.header("Content-Type", "application/json");
}
request.body(body);
}
switch (method.trim().toUpperCase()) {
case "GET":
return request.get(url);
case "POST":
return request.post(url);
case "PUT":
return request.put(url);
case "PATCH":
return request.patch(url);
case "DELETE":
return request.delete(url);
default:
throw new IllegalArgumentException("Unsupported HTTP method: " + method);
}
}
Step 6: Create a Token Generation using Playwright
In this step, we automate the process of generating authentication tokens using Playwright. Many APIs require login-based tokens (like cookies or bearer tokens), and managing them manually can be difficult — especially when they expire frequently.
The TokenManager class handles this by:
Logging into the application automatically using Playwright.
Extracting authentication cookies (OauthHMAC, OauthExpires, BearerToken).
Storing the token in a local JSON file for reuse.
Refreshing the token automatically when it expires.
This ensures that your API tests always use a valid token without manual updates, making the framework fully automated and CI/CD ready.
package org.Spurqlabs.Utils;
import java.io.*;
import java.nio.file.*;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Cookie;
public class TokenManager {
private static final ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<Long> expiryThreadLocal = new ThreadLocal<>();
private static final String TOKEN_FILE = "token.json";
private static final long TOKEN_VALIDITY_SECONDS = 30 * 60; // 30 minutes
public static String getToken() {
String token = tokenThreadLocal.get();
Long expiry = expiryThreadLocal.get();
if (token == null || expiry == null || Instant.now().getEpochSecond() >= expiry) {
// Try to read from a file (for multi-JVM/CI)
Map<String, Object> fileToken = readTokenFromFile();
if (fileToken != null) {
token = (String) fileToken.get("token");
expiry = ((Number) fileToken.get("expiry")).longValue();
}
// If still null or expired, fetch new
if (token == null || expiry == null || Instant.now().getEpochSecond() >= expiry) {
Map<String, Object> newToken = generateAuthTokenViaBrowser();
token = (String) newToken.get("token");
expiry = (Long) newToken.get("expiry");
writeTokenToFile(token, expiry);
}
tokenThreadLocal.set(token);
expiryThreadLocal.set(expiry);
}
return token;
}
private static Map<String, Object> generateAuthTokenViaBrowser() {
String bearerToken;
long expiry = Instant.now().getEpochSecond() + TOKEN_VALIDITY_SECONDS;
int maxRetries = 2;
int attempt = 0;
Exception lastException = null;
while (attempt < maxRetries) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
BrowserContext context = browser.newContext();
Page page = context.newPage();
// Robust wait for login page to load
page.navigate(FrameworkConfigReader.getFrameworkConfig("BaseUrl"), new Page.NavigateOptions().setTimeout(60000));
page.waitForSelector("#email", new Page.WaitForSelectorOptions().setTimeout(20000));
page.waitForSelector("#password", new Page.WaitForSelectorOptions().setTimeout(20000));
page.waitForSelector("button[type='submit']", new Page.WaitForSelectorOptions().setTimeout(20000));
// Fill a login form
page.fill("#email", FrameworkConfigReader.getFrameworkConfig("UserEmail"));
page.fill("#password", FrameworkConfigReader.getFrameworkConfig("UserPassword"));
page.waitForSelector("button[type='submit']:not([disabled])", new Page.WaitForSelectorOptions().setTimeout(10000));
page.click("button[type='submit']");
// Wait for either dashboard element or flexible URL match
boolean loggedIn;
try {
page.waitForSelector(".dashboard, .main-content, .navbar, .sidebar", new Page.WaitForSelectorOptions().setTimeout(20000));
loggedIn = true;
} catch (Exception e) {
// fallback to URL check
try {
page.waitForURL(url -> url.startsWith(FrameworkConfigReader.getFrameworkConfig("BaseUrl")), new Page.WaitForURLOptions().setTimeout(30000));
loggedIn = true;
} catch (Exception ex) {
// Both checks failed
loggedIn = false;
}
}
if (!loggedIn) {
throw new RuntimeException("Login did not complete successfully: dashboard element or expected URL not found");
}
// Extract cookies
String oauthHMAC = null;
String oauthExpires = null;
String token = null;
for (Cookie cookie : context.cookies()) {
switch (cookie.name) {
case "OauthHMAC":
oauthHMAC = cookie.name + "=" + cookie.value;
break;
case "OauthExpires":
oauthExpires = cookie.name + "=" + cookie.value;
if (cookie.expires != null && cookie.expires > 0) {
expiry = cookie.expires.longValue();
}
break;
case "BearerToken":
token = cookie.name + "=" + cookie.value;
break;
}
}
if (oauthHMAC != null && oauthExpires != null && token != null) {
bearerToken = oauthHMAC + ";" + oauthExpires + ";" + token + ";";
} else {
throw new RuntimeException("❗ One or more cookies are missing: OauthHMAC, OauthExpires, BearerToken");
}
browser.close();
Map<String, Object> map = new HashMap<>();
map.put("token", bearerToken);
map.put("expiry", expiry);
return map;
} catch (Exception e) {
lastException = e;
System.err.println("[TokenManager] Login attempt " + (attempt + 1) + " failed: " + e.getMessage());
attempt++;
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
}
}
throw new RuntimeException("Failed to generate auth token after " + maxRetries + " attempts", lastException);
}
private static void writeTokenToFile(String token, long expiry) {
try {
Map<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("expiry", expiry);
String json = new Gson().toJson(map);
Files.write(Paths.get(TOKEN_FILE), json.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
private static Map<String, Object> readTokenFromFile() {
try {
Path path = Paths.get(TOKEN_FILE);
if (!Files.exists(path)) return null;
String json = new String(Files.readAllBytes(path));
return new Gson().fromJson(json, new TypeToken<Map<String, Object>>() {}.getType());
} catch (IOException e) {
return null;
}
}
}
Step 7: Create Framework Config File
A good tester is one who knows the use and importance of config files. In this framework, we are also going to use the config file. Here, we are just going to put the base URL in this config file and will be using the same in the utility file over and over again. The config file contains more data than just of base URL when you start exploring the framework and start automating the new endpoints then at some point, you will realize that some data can be added to the config file.
Additionally, the purpose of the config files is to make tests more maintainable and reusable. Another benefit of a config file is that it makes the code more modular and easier to understand as all the configuration settings are stored in a separate file and it makes it easier to update the configuration settings for all the tests at once.
At this stage, we create the TestRunner class, which serves as the entry point to execute all Cucumber feature files. It uses TestNG as the test executor and integrates Cucumber for running BDD-style test scenarios.
The @CucumberOptions annotation defines:
features → Location of all .feature files.
glue → Packages containing step definitions and hooks.
plugin → Reporting options like JSON and HTML reports.
After execution, Cucumber automatically generates:
Cucumber.json → For CI/CD and detailed reporting.
Cucumber.html → A user-friendly HTML report showing test results.
This setup makes it easy to run all API tests and view clean, structured reports for quick analysis.
Once the framework is set up, you can execute your API automation suite directly from the command line using Maven. Maven handles compiling, running tests, and generating reports automatically.
Run All Tests –
To run all Cucumber feature files:
mvn clean test
clean → Deletes old compiled files and previous reports for a fresh run.
test → Executes all test scenarios defined in your project.
After running this command, Maven will trigger the Cucumber TestRunner, execute all scenarios, and generate reports in the test-output folder.
Run Tests by Tag –
Tags allow you to selectively run specific test scenarios or features. You can add tags like @api1, @smoke, or @regression in your .feature files to categorize tests.
Example:
@api1
Scenario: Verify POST API creates a record successfully
Given User sends "POST" request to "/api/v1/create" ...
Then User verifies the response status code is 201
To execute only scenarios with a specific tag, use:
mvn clean test -Dcucumber.filter.tags="@api1"
The framework will run only those tests that have the tag @api1.
You can combine tags for more flexibility:
@api1 or @api2 → Runs tests with either tag.
@smoke and not @wip → Runs smoke tests excluding work-in-progress scenarios.
This is especially useful when running specific test groups in CI/CD pipelines.
View Test Reports
API Automation Testing Framerwork Report – After the execution, Cucumber generates detailed reports automatically in the test-output directory:
Cucumber.html → User-friendly HTML report showing scenario results and logs.
Cucumber.json → JSON format report for CI/CD integrations or analytics tools.
You can open the report in your browser:
project-root/test-output/Cucumber.html
This section gives testers a clear understanding of how to:
API automation testing framework ensures that backend services are functioning properly before the application reaches the end user. Therefore, by integrating Cucumber, RestAssured, and Playwright, we have built a flexible and maintainable test framework that:
Supports BDD style scenarios.
Handles token-based authentication automatically.
Provides reusable utilities for API calls.
Generates rich HTML reports for easy analysis.
This hybrid setup helps QA engineers achieve faster feedback, maintain cleaner code, and enhance the overall quality of the software.
I am an Jr. SDET Engineer skilled in Manual and Automation Testing (UI & API). Proficient in Selenium, Cucumber, TestNG, Postman, RestAssured, Maven, SQL, GitHub, Jenkins, Java, JavaScript, HTML, and CSS. Experienced in CI/CD integration, framework design, and ensuring high-quality software delivery.
Manual Testing with Playwright MCP – Have you ever felt that a simple manual test should be less manual?
For years, quality assurance relied on pure human effort to explore, click, and record. But what if you could perform structured manual and exploratory testing, generate detailed reports, and even create test cases—all inside your Integrated Development Environment (IDE), using zero code?
I’ll tell you this: there’s a tool that can help us perform manual testing in a much more structured and easy way inside the IDE: Playwright MCP.
Section 1: End the Manual Grind – Welcome to AI-Augmented QA
The core idea is to pair a powerful AI assistant (like GitHub Copilot) with a tool that can control a real browser (Playwright MCP). This simple setup is done in only a few minutes.
The Essential Setup for Manual Testing with Playwright MCP: Detailed Steps
For this setup, you will integrate Playwright MCP as a tool that your AI agent can call directly from VS Code.
1. Prerequisites (The Basics)
VS Code installed in your system.
Node.js (LTS version recommended) installed on your machine.
2. Installing GitHub Copilot (The AI Client)
Open Extensions: In VS Code, navigate to the Extensions view (Ctrl+Shift+X or Cmd+Shift+X).
Search and Install: Search for “GitHub Copilot” and “GitHub Copilot Chat” and install both extensions.
Authentication: Follow the prompts to sign in with your GitHub account and activate your Copilot subscription.
GitHub Copilot is an AI-powered code assistant that acts almost like an AI pair programmer.
After successful installation and Authentication, you see something like below
3. Installing the Playwright MCP Server (The Browser Tool)
Playwright MCP (Model Context Protocol): This is the bridge that provides browser automation capabilities, enabling the AI to interact with the web page.
The most direct way to install the server and configure the agent is via the official GitHub page:
Navigate to the Source: Open your browser and search for the Playwright MCP Server official GitHub page (https://github.com/microsoft/playwright-mcp).
The One-Click Install: On the GitHub page, look for the Install Server VSCode button.
Launch VS Code: Clicking this button will prompt you to open Visual Studio Code.
Final Step: Inside VS Code, select the “Install server” option from the prompt to automatically add the MCP entry to your settings.
To verify successful installation and configuration, follow these steps:
Click on “Configure Tool” icon
After clicking on the “configure tool “ icon, you see the tools of Playwright MCP as shown in the below image.
After clicking on the “Settings” icon, you see the “Configuration (JSON)” file of Playwright MCP, where you start, stop, and restart the server as shown in image below
After the Playwright MCP Server is successfully configured and installed, you will see the output as shown below.
2. Stop and Restart Server
This complete setup allows the Playwright MCP Server to act as the bridge, providing browser automation capabilities and enabling the GitHub Copilot Agent to interact with the web page using natural language.
Section 2: Phase 1: Intelligent Exploration and Reporting
The first, most crucial step is to let the AI agent, powered by the Playwright MCP, perform the exploratory testing and generate the foundational report. This immediately reduces the tester’s documentation effort.
Instead of manually performing steps, you simply give the AI Agent your test objective in natural language.
The Exploration Workflow:
Exploration Execution: The AI uses discrete Playwright MCP tools (like browser_navigate, browser_fill, and browser_click) to perform each action in a real browser session.
Report Generation: Immediately following execution, the AI generates an Exploratory Testing Report. This report is generated on the basis of the exploration, summarizing the detailed steps taken, observations, and any issues found.
Our focus is simple: Using Playwright MCP, we reduce the repetitive tasks of a Manual Tester by automating the recording and execution of manual steps.
Execution Showcase: Exploration to Report
Input (The Prompt File for Exploration)
This prompt directs the AI to execute the manual steps and generate the initial report.
Prompt for Exploratory Testing
Exploratory Testing: (Use Playwright MCP)
Navigate to https://www.demoblaze.com/. Use Playwright MCP Compulsory for Exploring the Module <Module Name> and generate the Exploratory Testing Report in a .md file in the Manual Testing/Documentation Directory.
Output (The Generated Exploration Report) The AI generates a structured report summarizing the execution.
Live Browser Snapshot from Playwright MCP Execution
Once the initial Exploration Report is generated, QA teams move to design specific, reusable assets based on these findings.
1. Test Case Design (on basis of Exploration Report)
The Exploration Report provides the evidence needed to design formal Test Cases. The report’s observations are used to create the Expected Results column in your CSV or Test Management Tool.
The focus is now on designing reusable test cases, which can be stored in a CSV format.
These manually designed test cases form the core of your execution plan.
We need to provide the Exploratory Report for References at the time of design test Cases.
Drag and drop the Exploratory Report File as context as shown in the image below.
Input (Targeted Execution Prompt)
This prompt instructs the AI to perform a single, critical verification action from your Test Case.
Role: Act as a QA Engineer.
Based on Exploratory report Generate the Test cases in below of Format of Test Case Design Template
=======================================
🧪 TEST CASE DESIGN TEMPLATE For CSV File
=======================================
Test Case ID – Unique identifier for the test case (e.g., TC_001)
Test Case Title / Name – Short descriptive name of what is being tested
Preconditions / Setup – Any conditions that must be met before test execution
Test Data – Input values or data required for the test
Test Steps – Detailed step-by-step instructions on how to perform the test
Expected Result – What should happen after executing the steps
Actual Result – What happened (filled after execution)
Status – Pass / Fail / Blocked (result of the execution)
Priority – Importance of the test case (High / Medium / Low)
Severity – Impact level if the test fails (Critical / Major / Minor)
Test Type – (Optional) e.g., Functional, UI, Negative, Regression, etc.
Execution Date – (Optional) When the test was executed
Executed By – (Optional) Name of the tester
Remarks / Comments – Any additional information, observations, or bugs found
Output (The Generated Test cases)
The AI generates structured test cases.
2. Test Plan Creation
The created test cases are organized into a formal Test Plan document, detailing the scope, environment, and execution schedule.
Input (Targeted Execution Prompt)
This prompt instructs the AI to perform a single, critical verification action from your Test Case. 2
Role: Act as a QA Engineer. - Use clear, professional language. - Include examples where relevant. - Keep the structure organized for documentation. - Format can be plain text or Markdown. - Assume the project is a web application with multiple modules. generate Test Cases in Form Of <Module Name >.txt in Manual Testing/Documentation Directory Instructions for AI: - Generate a complete Test Plan for a software project For Our Test Cases - Include the following sections: 1. Test Plan ID 2. Project Name 3. Module/Feature Overview 4. Test Plan Description 5. Test Strategy (Manual, Automation, Tools) 6. Test Objectives 7. Test Deliverables 8. Testing Schedule / Milestones 9. Test Environment 10. Roles & Responsibilities 11. Risk & Mitigation 12. Entry and Exit Criteria 13. Test Case Design Approach 14. Metrics / Reporting 15. Approvals
Output (The Generated Test plan)
The AI generates structured test plan of designed test cases.
3. Test Cases Execution
This is where the Playwright MCP delivers the most power: executing the formal test cases designed in the previous step.
Instead of manually clicking through the steps defined in the Test Plan, the tester uses the AI agent to execute the written test case (e.g., loaded from the CSV) in the browser.
The Playwright MCP ensures the execution of those test cases is fast, documented, and accurate.
Any failures lead to immediate artifact generation (e.g., defect reports).
Input (Targeted Execution Prompt)
This prompt instructs the AI to perform a single, critical verification action from your Test Case.
Use Playwright MCP to Navigate “https://www.demoblaze.com/” and Execute Test Cases attached in context and Generate Test Execution Report.
First, Drag and drop the test case file for references as shown in the image below.
Live Browser Snapshot from Playwright MCP Execution
Output (The Generated Test Execution report)
The AI generates structured test execution report of designed test cases.
4. Defect Reporting and Tracking
If a Test Case execution fails, the tester immediately leverages the AI Agent and Playwright MCP to generate a detailed defect report, which is a key task in manual testing.
Execution Showcase: Formal Test Case Run (with Defect Reporting)
We will now execute a Test Case step, intentionally simulating a failure to demonstrate the automated defect reporting capability.
Input (Targeted Execution Prompt for Failure)
This prompt asks the AI to execute a check and explicitly requests a defect report and a screenshot if the assertion fails.
Refer to the test cases provided in the Context and Use Playwright MCP to execute the test, and if there is any defect, then generate a detailed defect Report. Additionally, I would like a screenshot of the defect for evidence.
Output (The Generated Defect report and Screenshots as Evidence)
The AI generates a structured defect report of designed test cases.
Conclusion: Your Role is Evolving, Not Ending
Manual Testing with Playwright MCP is not about replacing the manual tester; it’s about augmenting their capabilities. It enables a smooth, documented, and low-code way to perform high-quality exploratory testing with automated execution.
Focus on Logic: Spend less time on repetitive clicks and more time on complex scenario design.
Execute Instantly: Use natural language prompts to execute tests in the browser.
Generate Instant Reports: Create structured exploratory test reports from your execution sessions.
Future-Proof Your Skills: Learn to transition seamlessly to an AI-augmented testing workflow.
It’s time to move beyond the traditional—set up your Playwright MCP today and start testing with the power of an AI-pair tester!
Automation always comes with surprises. Recently, I stumbled upon one such challenge while working on a scenario that required automating PDF download using Playwright to verify a PDF download functionality. Sounds straightforward, right? At first, I thought so too. But the web application I was dealing with had other plans.
The Unexpected Complexity
Instead of a simple file download, the application displayed the report PDF inside an iframe. Looking deeper, I noticed a blob source associated with the PDF. Initially, it felt promising—maybe I could just fetch the blob and save it. But soon, I realized the blob didn’t actually contain the full PDF file. It only represented the layout instructions, not the content itself.
Things got more interesting (and complicated) when I found out that the entire PDF was rendered inside a canvas. The content wasn’t static—it was dynamically displayed page by page. This meant I couldn’t directly extract or save the file from the DOM.
At this point, downloading the PDF programmatically felt like chasing shadows.
The Print Button Dilemma
To make matters trickier, the only straightforward option available on the page was the print button. Clicking it triggered the system’s file explorer dialog, asking me to manually pick a save location. While that works fine for an end-user, for automation purposes it was a dealbreaker.
I didn’t want my automation scripts to depend on manual interaction. The whole point of this exercise was to make the process seamless and repeatable.
Digging Deeper: A Breakthrough
After exploring multiple dead ends, I finally turned my focus back to Playwright itself. That’s when I discovered something powerful—Playwright’s built-in capability to generate PDFs directly from a page.
The key was:
Wait for the report to open in a new tab (triggered by the app after selecting “Print View”).
Bring this new page into focus and make sure all content was fully rendered.
Use Playwright’s page.pdf() function to export the page as a properly styled PDF file.
The Solution in Action
Here’s the snippet that solved it:
// Wait for new tab to open and capture it
const [newPage] = await Promise.all([
context.waitForEvent("page"),
event.Click("(//span[text()='OK'])[1]", page), // triggers tab open
]);
global.secondPage = newPage;
await global.secondPage.bringToFront();
await global.secondPage.waitForLoadState("domcontentloaded");
// Use screen media for styling
await global.secondPage.emulateMedia({ media: "screen" });
// Path where you want the file saved
const downloadDir = path.resolve(__dirname, "..", "Downloads", "Reports");
if (!fs.existsSync(downloadDir)) fs.mkdirSync(downloadDir, { recursive: true });
const filePath = path.join(downloadDir, "report.pdf");
// Save as PDF
await global.secondPage.pdf({
path: filePath,
format: "A4",
printBackground: true,
margin: {
top: "1cm",
bottom: "1cm",
left: "1cm",
right: "1cm",
},
});
console.log(`✅ PDF saved to: ${filePath}`);
Key Highlights of the Implementation
Capturing the New Tab The Print/PDF Report option opened the report in a new browser tab. Instead of losing control, we captured it with context.waitForEvent(“page”) and stored it in a global variable global.secondPage. This ensured smooth access to the report tab for further processing.
Switching to Print View The dropdown option was switched to Print View to ensure the PDF was generated in the correct layout before proceeding with export.
Emulating Screen Media To preserve the on-screen styling (instead of print-only styles), we used page.emulateMedia({ media: “screen” }). This allowed the generated PDF to look exactly like what users see in the browser.
Saving the PDF to a Custom Path A custom folder structure was created dynamically using Node.js path and fs modules. The PDFs were named systematically and stored under Downloads/ImageTrend/<date>/, ensuring organized storage.
Full-Page Export with Print Background Using Playwright’s page.pdf() method, we captured all pages of the report (not just the visible one), along with background colors and styles for accurate representation.
Clean Tab Management Once the PDF was saved, the secondary tab (global.secondPage) was closed, bringing the focus back to the original tab for processing the next incident report.
What I Learned
This challenge taught me something new: PDFs in web apps aren’t always what they seem. Sometimes they’re iframes, sometimes blob objects, and in trickier cases, dynamically rendered canvases. Trying to grab the raw file won’t always work.
But with Playwright, there’s a smarter way. By leveraging its ability to generate PDFs from a live-rendered page, I was able to bypass the iframe/blob/canvas complexity entirely and produce consistent, high-quality PDF files.
Conclusion:
What started as a simple “verify PDF download” task quickly turned into a tricky puzzle of iframes, blobs, and canvases. But the solution I found—automating PDF download using Playwright with its built-in PDF generation—was not just a fix, it was an eye-opener.
It reminded me once again that automation isn’t just about tools; it’s about understanding the problem deeply and then letting the tools do what they do best.
This was something new I learned, and I wanted to share it with all of you. Hopefully, it helps the next time you face a similar challenge.
Behavior Driven Development (BDD) is a process that promotes collaboration between developers, testers, and stakeholders by writing test cases in simple, plain language. BDD Automation Frameworks like Cucumber use Gherkin to make test scenarios easily understandable and link them to automated tests.
In this guide, we’ll show you how to create a BDD Automation Framework using Java and Playwright. Playwright is a powerful browser automation tool, and when combined with Java and Cucumber, it creates a solid BDD testing framework.
Introduction to BDD Automation Framework:
Automation testing is testing software with the latest tools and technologies with developed scripts in less time. In Automation testing it involves test case execution, data validation, and result reporting.
Why Playwright over Selenium?
Playwright is an open-source Node.js library that further enables efficient end-to-end (E2E) testing of web applications. As Playwright offers better performance speed than Selenium. Also, Playwright offers various features like Cross-Brower support, Multi-platform, Headless and Headful Mode, Async/Await API, Integration with Testing Frameworks.
What is BDD Automation Framework?
BDD framework is an agile approach to test software where testers write test cases in simple language so that non-tech person can also understand the flow. Moreover, it enhances collaboration between the technical team and the business team. We use Gherkin language to write feature files, making them easily readable by everyone.
Prerequisites for BDD Automation Framework:
1. Install JDK
Install the Java environment as per the system compatible.
First, choose the appropriate JDK version, and then click on the download link for the Windows version.
Run the Installer:
Once the download is complete, run the installer.
To begin, follow the installation instructions, then accept the license agreement, and finally choose the installation directory.
Set Environment Variables:
Open the Control Panel and go to System and Security > System > Advanced system settings.
Click on “Environment Variables”.
Under “System Variables,” click on “New” and add a variable named JAVA_HOME with the path to the JDK installation directory (e.g., C:\Program Files\Java\jdk-15).
Find the “Path” variable in the “System Variables” section, click on “Edit,” and add a new entry with the path to the bin directory inside the JDK installation directory (e.g., C:\Program Files\Java\jdk-15\bin).
Verify Installation:
Open a Command Prompt and check if Java is installed correctly by typing `java -version` and `javac -version`.
Click on the link to download the binary zip archive (e.g., apache-maven-3.x.y-bin.zip).
Extract the Archive:
Extract the downloaded zip file to a suitable directory (e.g., C:\Program Files\Apache\maven).
Set Environment Variables:
Open the Control Panel and go to System and Security > System > Advanced system settings.
Click on “Environment Variables”.
Under “System Variables”, click on “New” and add a variable named MAVEN_HOME with the path to the Maven installation directory (e.g., C:\Program Files\Apache\maven\apache-maven-3.x.y).
Find the “Path” variable in the “System Variables” section, click on “Edit”, and add a new entry with the path to the bin directory inside the Maven installation directory (e.g., C:\Program Files\Apache\maven\apache-maven-3.x.y\bin).
Verify Installation:
To check if Maven is installed correctly, open a Command Prompt and type `mvn -version`.
Java Development Kit (JDK): Ensure you have JDK installed and properly configured.
Maven or Gradle: Depending on your preference, however, you’ll need Maven or Gradle to manage your project dependencies.
Steps to Install Cucumber with Maven
Create a Maven Project:
Update pom.xml File:
Open the pom.xml file in your project.
This Maven POM file (pom.xml) defines project metadata, dependencies on external libraries (Cucumber, Selenium, Playwright), and Maven build properties. It provides the necessary configuration for managing dependencies, compiling Java source code, and integrating with Cucumber, TestNG, Selenium, and Playwright frameworks to support automated testing and development of the CalculatorBDD project.
Before starting the project on the BDD Automation Framework:
Create a new Maven project in your IDE.
Add the dependencies in Pom.xml file .
Create folder structure following steps given below:
When we created the new project for the executable jar file, we could see the simple folder structure provided by Maven.
SRC Folder: The SRC folder is the parent folder of a project, and it will also include the main and test foldersIn the QA environment, we generally use the test folder, while we reserve the main folder for the development environment. The development team uses the main folder, so the created JAR contains all the files inside the src folder.
Test Folder: Inside the test folder; additionally, Java and resources folders are available.
Java Folder: This folder primarily contains the Java classes where the actual code is present.
Resources Folder: The Resources folder contains the resources file, test data file, and document files.
Pom.xml: In this file, we are managing the dependencies and plugins that are required for automation.
As our project structure is ready so we can start with the BDD framework:
1. Feature file:
Here we have described the scenario in “Gherkin” language which is designed to be easily understandable by non-technical stakeholders as well as executable by automation tools like Cucumber. Each scenario is written in structured manner using keywords “Given”, “When” and “Then”. Calculator.feature in this we have specifically written our functional testing steps.
@Basic
Feature: Verify Calculator Operations
Scenario Outline: Verify addition of two numbers
#Given line states that the User is on the Calculator home page and Calculator page is displayed.
Given I am on Calculator page
#When step describes an action that User enters/clicks on a number
When I enter number <number>
#And step indicates clicking on a specific operator (like addition, subtraction, etc.) on the calculator
And I click on operator '<operator>'
#And Step follows the operator click by entering another number into the calculator.
And I enter number <number1>
#Then is the verification step where the test checks if the result displayed
Then I verify the result as <expectedResult>
Examples:
| number | operator | number1 | expectedResult |
| 5 | + | 2 | 7 |
| 9 | - | 3 | 6 |
| 6 | * | 4 | 24 |
| 2 | / | 2 | 1 |
2. Step Def File:
The step definition file serves as the bridge between actual feature file with the actual method implementation in the page file. The Calculator steps are a step definition file that maps the feature file to the page file and functional implementation.
package steps;
import core.TestContext;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.testng.Assert;
import pages.CalculatorPage;
import java.io.IOException;
public class CalculatorSteps extends TestContext {
public CalculatorSteps() {
//Here the constructor initializes a new instance of CalculatorPage,
// so the CalculatorSteps class can interact with the calculator web page through methods in the CalculatorPage class
calculatorPage = new CalculatorPage();
}
@Given("I am on Calculator page")
//Here we call the function from Calculator Page which is Sync with Feature file Given definition
public void iAmOnCalculatorPage() throws IOException {
calculatorPage.iAmOnCalculatorPage();
}
@When("I enter number {int}")
//Here also the function is called from page file and Sync with feature file When step
public void iEnterNumber(int number) {
calculatorPage.iEnterNumber(number);
}
@And("I click on operator {string}")
//Here also the function is called from page file and sync with feature file And step
public void iClickOnOperator(String operator) {
calculatorPage.iClickOnOperator(operator);
}
@Then("I verify the result as {int}")
//Here also the function is called from page file and synched with feature file Then step
public void iVerifyTheResultAs(int expectedResult) {
String actualResult = calculatorPage.iVerifyTheResultAs();
Assert.assertEquals(actualResult, String.valueOf(expectedResult));
}
}
3. Page File:
Page file, in addition, is actual code implementation from the step definition file.Here, we have saved all the actual methods and web page elements, thereby ensuring easy access and organization. It is basically POM structure. So here we are performing addition operation in Calculator we application so created a method to click on a number and another method for clicking on the operator. Here we can minimize the code by reusing the code as much as possible.
package pages;
import core.TestContext;
import utilities.ConfigUtil;
import java.io.IOException;
public class CalculatorPage extends TestContext {
public void iAmOnCalculatorPage() throws IOException {
page.navigate(ConfigUtil.getPropertyValue("base_url"));
}
public void iEnterNumber(int number) {
page.locator("//span[@onclick='r(" + number + ")']").click();
}
public void iClickOnOperator(String operator) {
page.locator("//span[@onclick=\"r('" + operator + "')\"]").click();
}
public String iVerifyTheResultAs() {
page.locator("//span[@onclick=\"r('=')\"]").click();
return page.locator("//div[@id='sciOutPut']").innerText().trim();
}
public void tearDown() {
page.close();
}
}
4. Hooks:
Hooks are setup and teardown methods that, therefore, are written separately in the configuration class. Here we have annotation declare in the hooks file @before and @After. Hooks are steps to be performed a before and after function of the feature file. In this we have open the Web browser in Before and After Tag. These are special functions which allows the testers to execute specific points during execution.
package core;
import core.TestContext;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
import utilities.WebUtil;
public class Hooks extends TestContext {//Hooks class inherits the property of TestContext class
@Before //@Before Tag denotes that it should be executed before scenario
public void beforeScenario(Scenario scenario) {
page = WebUtil.initBrowser(); //this method initializes the browser session
}
@After //@After Tag denotes that it should be executed after scenario
public void afterScenario() {
WebUtil.tearDownPW();//this method is for tasks such as closing browser sessions
}
}
5. TestContext:
The TestContext class, moreover, holds various instances and variables required for test execution. In this context, we have successfully created a web driver instance, a page file instance, and a browser context. As a result, the code reusability, organization, and maintainability are improved here.
package core;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import pages.CalculatorPage;
public class TestContext { //TestContext class, which acts as a container to store all instances for test framework
public static Page page;
//Refers to Playwright’s Page object. This controls a specific browser tab or page in a Playwright-based test
public static CalculatorPage calculatorPage;
//This stores an instance of the CalculatorPage object, representing the page object model (POM)
public static Browser browser;
//refers to Playwright's Browser instance, which represents the entire browser
}
6. TestRunner:
The Test Runner is responsible for discovering test cases, executing them, and reporting the results back; additionally, it provides the necessary infrastructure to execute the tests and manage the testing workflow. It also syncs the feature file with step file.
package core;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
import org.testng.annotations.DataProvider;
@CucumberOptions(features = "src/test/java/features", //the path where Cucumber feature files are located.
glue = {"steps", "core"}) //Cucumber where to find the step definitions (in the steps and core packages)
public class TestRunner extends AbstractTestNGCucumberTests {
//Above Etends which is a base class provided by Cucumber to run the tests with TestNG
@DataProvider //allows running multiple Cucumber scenarios as separate tests in TestNG
@Override
public Object[][] scenarios() {
return super.scenarios();
}//Calls the parent class method to return all the Cucumber scenarios in an array format for TestNG to run
}
7. WebUtils:
Web Utils is a file in which browser instance is created and playwright is initialised here. The code for web browser page launching is written here and for closing the browser instance. The page is extended by TestContext where all the properties of TestContext are given to WebUtils page.
package utilities;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import core.TestContext;
public class WebUtil extends TestContext {
public static Page initBrowser(){
//Initializes a browser session using Playwright's Chromium browser
Playwright playwright = Playwright.create(); //Creates an instance of Playwright
browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false));
page = browser.newPage(); //Creates a new page/tab within the launched browser
return page;
}
public static void tearDownPW() {
page.close();
} // It is called to close the current page/tag
}
This is the important file where we download all the dependencies required for the test execution. Also, it contains information of project and configuration information for the maven to build the project such as dependencies, build directory, source directory, test source directory, plugin, goals etc.
In this blog, we’ve discussed using the Java Playwright framework with Cucumber for BDD. Playwright offers fast, cross-browser testing and easy parallel execution, making it a great alternative to Selenium. Paired with Cucumber, it helps teams write clear, automated tests. Playwright’s debugging tools and test isolation also reduce test issues and maintenance, making it ideal for building reliable test suites for faster, higher-quality software delivery.