tm-playwright-framework
Version:
Playwright Cucumber TS framework - The easiest way to learn
304 lines (303 loc) • 12.3 kB
JavaScript
/**
* PlaywrightRequestBuilder
*
* This file defines the `PlaywrightRequestBuilder` class, which provides a fluent API for building and sending HTTP requests
* using Playwright's `APIRequestContext`. It supports various HTTP methods (GET, POST, PUT, DELETE) and allows customization
* of request options such as headers, query parameters, authentication, and more.
*
* Key Features:
* - Fluent API for setting request options (e.g., headers, timeout, method, URL, etc.).
* - Support for request templates to standardize API calls.
* - Retry logic for handling transient failures.
* - Integration with Playwright's `APIRequestContext` for sending requests.
* - Automatic response handling and logging.
* - Assertion of expected status codes and response validation.
*
* Usage:
* 1. Create an instance of `PlaywrightRequestBuilder`.
* 2. Chain methods to configure the request (e.g., `setMethod`, `setURL`, `setHeaders`).
* 3. Call `send()` or `sendWithRetries()` to execute the request.
*
* Example:
* ```typescript
* const requestBuilder = new PlaywrightRequestBuilder();
* await requestBuilder
* .setMethod('GET')
* .setURL('https://example.com/api')
* .setHeaders({ Authorization: 'Bearer token' })
* .send();
* ```
*
* Dependencies:
* - Playwright for HTTP request handling.
* - JSONPath for extracting data from JSON templates.
* - fs-extra for file system operations.
* - qs for query string manipulation.
* - tm-playwright-framework for integration with the testing framework.
*
*
* @author Sasitharan, Govindharam
* @reviewer Sahoo, AshokKumar
* @version 1.0 - 1st-JUNE-2025
*/
import { JSONPath } from 'jsonpath-plus';
import { request } from 'playwright';
import fs from "fs-extra";
import qs from 'querystring';
import { fixture } from 'tm-playwright-framework/dist/hooks/pageFixture.js';
import { buildRequest, writeResponse } from 'tm-playwright-framework/dist/api/api_utility.js';
import { expect } from '@playwright/test';
import config from 'tm-playwright-framework/dist/config/playwright.config.js';
let baseURL;
class PlaywrightRequestBuilder {
constructor() {
this.options = {}; // Using a plain object for options
this.context = null; // Initialize context as null
}
// Initialize Playwright's request context
async initContext() {
if (!this.context) {
this.context = await request.newContext({});
}
return this.context;
}
// Set timeout in milliseconds
setTimeout(ms) {
this.options.timeout = ms;
return this;
}
// Set additional HTTP headers
setHeaders(headers) {
this.options.headers = {
...this.options.headers,
...headers,
};
return this;
}
updateURL(findWhat, replaceWith) {
try {
this.options.url = this.options.url?.replace(findWhat, replaceWith);
}
catch (Error) {
console.log(Error);
fixture.logger.error(Error.message);
fixture.logger.error(Error.stack);
}
return this;
}
setTemplate(templateName, appBaseURL) {
try {
if (!appBaseURL) {
if (!baseURL)
baseURL = config.use?.baseURL;
if (!baseURL)
baseURL = process.env.API_BASEURL;
}
else {
baseURL = appBaseURL;
}
this.options.template = templateName;
let fileData = JSON.parse(fs.readFileSync(`${process.env.API_TEMPLATE_PATH}/${templateName}.json`, "utf8"));
const contentType = JSONPath({ json: fileData, path: '$.Content-Type' })[0];
const accept = JSONPath({ json: fileData, path: '$.Accept' })[0];
const url = JSONPath({ json: fileData, path: '$.EndPoint' })[0];
this.options.method = JSONPath({ json: fileData, path: '$.Method' })[0];
if (url.toLowerCase().includes("http"))
this.options.url = `${url}`;
else
this.options.url = `${baseURL}${url}`;
this.setHeaders({ 'Content-Type': `${contentType}`, Accept: `${accept}` });
}
catch (Error) {
console.log(Error);
fixture.logger.error(Error.message);
fixture.logger.error(Error.stack);
}
return this;
}
// Set proxy server for the request
setProxy(proxy) {
this.options.proxy = { server: proxy };
return this;
}
// Set if request should fail on non-2xx status codes
setFailOnStatusCode(fail) {
this.options.failOnStatusCode = fail;
return this;
}
// Set the request method (e.g., GET, POST)
setMethod(method) {
this.options.method = method.toUpperCase();
return this;
}
// Set the request URL
setURL(url) {
this.options.url = url;
return this;
}
// Set the body of the request (for POST, PUT, etc.)
setForm(form) {
this.options.form = qs.stringify(buildRequest(form, this.options.template, this.options.iteration));
return this;
}
// Set the body of the request (for POST, PUT, etc.)
setBody(body) {
this.options.data = buildRequest(body, this.options.template, this.options.iteration); // Use 'data' for body content in Playwright API
return this;
}
// Set the Iteration of the request (for POST, PUT, etc.)
setIteration(iteration) {
this.options.iteration = iteration; // Use 'data' for body content in Playwright API
return this;
}
// Set query parameters for the request
setQueryParams(params) {
const url = new URL(this.options.url); // We assume URL is set before
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
this.options.url = url.toString();
return this;
}
// Set authentication (username and password)
setAuth(username, password) {
this.options.auth = { username, password };
return this;
}
// Set response type (used with custom logic for handling response data)
setResponseType(type) {
this.options.responseType = type;
return this;
}
// Set the maximum number of retries for the request
setRetries(retries) {
this.options.retries = retries;
return this;
}
// Set the maximum number of retries for the request
setExpectedStatusCode(expectedCode) {
this.options.expectedCode = expectedCode;
return this;
}
// Send the request using Playwright's APIRequestContext
async send() {
const context = await this.initContext();
// Make the request using the correct method (e.g., GET, POST, etc.)
let response;
switch (this.options.method) {
case 'GET':
response = await context.get(this.options.url, {
headers: this.options.headers,
timeout: this.options.timeout,
failOnStatusCode: this.options.failOnStatusCode,
});
fixture.logger.info("Executing API (GET): " + this.options.url);
break;
case 'POST':
if (this.options.form)
this.options.data = this.options.form.toString(); // Use form data for POST body
response = await context.post(this.options.url, {
headers: this.options.headers,
data: this.options.data, // Use 'data' for POST body
timeout: this.options.timeout,
failOnStatusCode: this.options.failOnStatusCode,
});
fixture.logger.info("Executing API (POST): " + this.options.url);
break;
case 'PUT':
response = await context.put(this.options.url, {
headers: this.options.headers,
data: this.options.data, // Use 'data' for PUT body
timeout: this.options.timeout,
failOnStatusCode: this.options.failOnStatusCode,
});
fixture.logger.info("Executing API (PUT): " + this.options.url);
break;
case 'DELETE':
response = await context.delete(this.options.url, {
headers: this.options.headers,
timeout: this.options.timeout,
failOnStatusCode: this.options.failOnStatusCode,
});
fixture.logger.info("Executing API (DELETE): " + this.options.url);
break;
default:
throw new Error(`Unsupported method: ${this.options.method}. Supported methods: GET, POST, PUT, DELETE`);
}
const contentType = response.headers()['content-type'];
let APIResponse;
if (contentType?.includes('application/json')) {
APIResponse = await response.json();
writeResponse(APIResponse, this.options.template, this.options.iteration, ".json");
}
else {
APIResponse = await response.text();
writeResponse(APIResponse, this.options.template, this.options.iteration, ".txt");
}
if (response.ok()) {
this.message = `<font color=green><b>PASS: Assertion has been passed. Expected and Received Status Code: ${this.options.expectedCode} </b></font>`;
}
else {
this.message = `<font color=red><b>FAIl: Assertion has been failed. Expected ${this.options.expectedCode} but Received: ${response.status()} </b></font>`;
}
if (this.options.expectedCode)
expect(response.status()).toBe(this.options.expectedCode);
expect(response.ok()).toBeTruthy();
fixture.logger.info(this.message);
this.reponse = APIResponse;
return response;
}
// Retry logic
async sendWithRetries(retries = 3, delayMs = 1000) {
let attempt = 0;
while (attempt < retries) {
try {
const response = await this.send();
return response;
}
catch (err) {
attempt++;
console.warn(`Attempt ${attempt} failed: ${err.message}`);
if (attempt === retries) {
throw new Error(`Request failed after ${retries} attempts: ${err.message}`);
}
await new Promise(res => setTimeout(res, delayMs)); // delay before retrying
}
}
throw new Error('Max retries exceeded');
}
async sendWithConditionRetries(sendFn, options) {
const { expectedStatus, jsonPath, expectedJsonValue, retries = 3, timeoutMs = 10000, delayMs = 1000, } = options;
const startTime = Date.now();
let attempt = 0;
while (attempt < retries && (Date.now() - startTime) < timeoutMs) {
attempt++;
try {
const response = await sendFn();
const statusOk = expectedStatus === undefined || response.status() === expectedStatus;
let valueOk = true;
if (jsonPath && expectedJsonValue !== undefined) {
const result = JSONPath({ path: jsonPath, json: response.body });
valueOk = result.length > 0 && result[0] === expectedJsonValue;
}
if (statusOk && valueOk) {
return response;
}
else {
console.warn(`Attempt ${attempt}: condition not met (status: ${response.status}, jsonPath match: ${valueOk}). Retrying...`);
}
}
catch (err) {
console.warn(`Attempt ${attempt} failed: ${err.message}`);
}
if (attempt < retries && (Date.now() - startTime) < timeoutMs) {
await sleep(delayMs);
}
}
throw new Error(`Request did not meet conditions after ${retries} attempts or timeout of ${timeoutMs}ms`);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export { PlaywrightRequestBuilder };