playwright-cucumber-ts-steps
Version:
A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.
326 lines (311 loc) • 12.1 kB
text/typescript
// e2e/step_definitions/common/interceptionSteps.ts
import { When, DataTable } from "@cucumber/cucumber";
import { CustomWorld } from "../helpers/world"; // Assuming this path is correct
// =============================
// WHEN I INTERCEPT / STUB
// =============================
/**
* Intercepts a network request to the given URL and stubs its response body with the provided JSON.
* This step should be used *before* the action that triggers the request (e.g., navigating to a page
* or clicking a button that makes an API call).
*
* ```gherkin
* When I intercept URL {string} and stub body:
* """
* { "foo": "bar" }
* """
* ```
*
* @param url - The URL to intercept. This can be a full URL, a URL glob, or a regular expression string.
* @param body - A DocString containing the JSON string to use as the response body.
*
* @example
* Scenario: Stubbing an API response
* When I intercept URL "https://api.example.com/data" and stub body:
* """
* { "status": "success", "items": ["item1", "item2"] }
* """
* And I go to "/dashboard"
* Then I should see text "item1"
*
* @remarks
* The intercepted request will respond with a `200 OK` status and a `Content-Type` of `application/json`.
* The `body` provided must be valid JSON. Playwright's `page.route()` is used internally.
* Make sure this step executes *before* the network request is initiated.
* @category Network Interception Steps
*/
export async function When_I_intercept_URL_and_stub_body(
this: CustomWorld,
url: string,
body: string
) {
let parsedBody: any;
try {
parsedBody = JSON.parse(body);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new Error(
`Failed to parse JSON body for URL "${url}": ${message}. Please provide valid JSON.`
);
}
await this.page.route(url, (route: import("playwright").Route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(parsedBody),
});
});
this.log?.(
`📡 Intercepted and stubbed URL "${url}" with JSON body: ${JSON.stringify(parsedBody).slice(0, 100)}...`
);
}
When("I intercept URL {string} and stub body:", When_I_intercept_URL_and_stub_body);
/**
* Intercepts a network request to the given URL and allows it to continue unmodified.
* This can be useful for ensuring that specific requests are *not* blocked or for logging
* requests without altering their behavior.
*
* ```gherkin
* When I intercept URL {string}
* ```
*
* @param url - The URL to intercept. This can be a full URL, a URL glob, or a regular expression string.
*
* @example
* Scenario: Allowing a specific request to pass through
* When I intercept URL "https://analytics.example.com/*"
* And I go to "/dashboard"
* Then the page title should be "Dashboard"
*
* @remarks
* This step uses Playwright's `page.route()` with `route.continue()`. No changes are made
* to the request or its response. This is essentially a "monitor" step.
* @category Network Interception Steps
*/
export async function When_I_intercept_URL(this: CustomWorld, url: string) {
await this.page.route(url, async (route) => {
await route.continue();
});
this.log?.(`📡 Intercepted URL "${url}" and allowed it to continue.`);
}
When("I intercept URL {string}", When_I_intercept_URL);
/**
* Intercepts a network request to the given URL and stubs its response body with a raw string.
* This is useful for returning non-JSON content or simple string responses.
*
* ```gherkin
* When I intercept URL {string} and stub body {string}
* ```
*
* @param url - The URL to intercept. This can be a full URL, a URL glob, or a regular expression string.
* @param body - The raw string content to use as the response body.
*
* @example
* Scenario: Stubbing a simple text response
* When I intercept URL "https://api.example.com/status" and stub body "OK"
* And I visit page with JS that fetches "/status"
* Then I should see text "OK"
*
* @remarks
* The intercepted request will respond with a `200 OK` status and a `Content-Type` of `application/json`
* by default (though the `body` is a raw string). If a different `contentType` is needed, consider
* using the `I intercept URL {string} and stub response with:` step if available, or extending this step.
* @category Network Interception Steps
*/
export async function When_I_intercept_URL_and_stub_raw_body(
this: CustomWorld,
url: string,
body: string
) {
await this.page.route(url, (route) => {
route.fulfill({
status: 200,
contentType: "application/json", // Defaulting to JSON, can be customized if needed
body,
});
});
this.log?.(`📡 Intercepted and stubbed URL "${url}" with raw body: "${body.slice(0, 100)}..."`);
}
When("I intercept URL {string} and stub body {string}", When_I_intercept_URL_and_stub_raw_body);
// =============================
// WHEN I MAKE REQUEST
// =============================
/**
* Makes a GET request to the given URL and stores the response details (status and body)
* in the test context.
*
* ```gherkin
* When I make request to {string}
* ```
*
* @param url - The URL to which the GET request will be made.
*
* @example
* Scenario: Verify API endpoint status
* When I make request to "https://api.example.com/health"
* Then the last response status should be 200
* And the last response body should contain "healthy"
*
* @remarks
* The response object (including status and body) is stored in
* {@link CustomWorld.data.lastResponse | this.data.lastResponse}. Subsequent assertion steps
* can then access this data.
* @category API Request Steps
*/
export async function When_I_make_GET_request_to(this: CustomWorld, url: string) {
this.log?.(`⚡ Making GET request to "${url}"...`);
const response = await this.page.request.get(url);
const status = response.status();
const body = await response.text();
this.data.lastResponse = { status, body }; // Storing as an object for clarity
this.log?.(
`✅ GET request to "${url}" completed. Status: ${status}. Body preview: ${body.slice(0, 100)}...`
);
}
// Note: You had two steps "I make request to" and "I make a request to".
// I'm keeping one for clarity and suggesting the other is an alias if needed.
When("I make request to {string}", When_I_make_GET_request_to);
When("I make a request to {string}", When_I_make_GET_request_to); // Alias for consistency
/**
* Makes a POST request to the given URL with the provided JSON body and stores the response details.
*
* ```gherkin
* When I make a POST request to {string} with JSON body:
* """
* { "username": "user", "password": "pass" }
* """
* ```
*
* @param url - The URL to which the POST request will be made.
* @param docString - A Cucumber DocString containing the JSON payload for the request body.
*
* @example
* Scenario: Login via API
* When I make a POST request to "https://api.example.com/login" with JSON body:
* """
* { "email": "test@example.com", "password": "password123" }
* """
* Then the last response status should be 200
* And the last response body should contain "token"
*
* @remarks
* The `docString` must contain valid JSON. The request will have a `Content-Type` of `application/json`.
* The response object (including status and body) is stored in
* {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
* @category API Request Steps
*/
export async function When_I_make_POST_request_with_JSON_body(
this: CustomWorld,
url: string,
docString: string
) {
let payload: any;
try {
payload = JSON.parse(docString);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new Error(`Invalid JSON payload for POST request to "${url}": ${message}.`);
}
this.log?.(`⚡ Making POST request to "${url}" with payload: ${docString.slice(0, 100)}...`);
const response = await this.page.request.post(url, { data: payload });
const status = response.status();
const body = await response.text();
this.data.lastResponse = { status, body };
this.log?.(
`✅ POST request to "${url}" completed. Status: ${status}. Body preview: ${body.slice(0, 100)}...`
);
}
When("I make a POST request to {string} with JSON body:", When_I_make_POST_request_with_JSON_body);
/**
* Makes a generic HTTP request with a specified method and optional headers/body from a data table.
*
* ```gherkin
* When I make a "{word}" request to {string}
* | header | value |
* | Content-Type | application/json |
* | ... | ... |
* ```
*
* @param method - The HTTP method (e.g., "GET", "POST", "PUT", "DELETE").
* @param url - The URL for the request.
* @param table - (Optional) A Cucumber DataTable with `header` and `value` columns for headers,
* and a `body` row for the request body.
*
* @example
* Scenario: Make a PUT request with custom headers
* When I make a "PUT" request to "https://api.example.com/resource/123"
* | Content-Type | application/json |
* | Authorization | Bearer my-token |
* | body | { "status": "updated" } |
* Then the last response status should be 200
*
* @remarks
* This step constructs a `fetch` request using the provided method, URL, and options from the
* data table. The `body` provided in the data table (if any) will be automatically
* JSON.parsed and then JSON.stringified. The entire `Response` object from `fetch` is
* stored in {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
*
* **Note on `fetch`:** This step uses the browser's `fetch` API, which runs in the browser context.
* If you need to make Node.js-based requests (e.g., for backend APIs that are not accessible from the browser),
* you might prefer `page.request` (as used in `When_I_make_GET_request_to` or `When_I_make_POST_request_with_JSON_body`).
* This step stores the raw `Response` object, so you might need to call `.json()` or `.text()`
* on `this.data.lastResponse` in subsequent steps if you need to assert on its content.
* @category API Request Steps
*/
export async function When_I_make_HTTP_request_with_options(
this: CustomWorld,
method: string,
url: string,
table?: DataTable
) {
const options: RequestInit = {};
if (table) {
const rows = table.rows();
rows.forEach((row) => {
const [key, val] = row;
if (key.toLowerCase() === "body") {
try {
options.body = JSON.stringify(JSON.parse(val)); // Parse then stringify for valid JSON body
} catch (e) {
throw new Error(`${e}Invalid JSON body provided in data table for request: ${val}`);
}
} else {
// Treat as headers for now. Can be expanded for other options (e.g., credentials, mode)
if (!options.headers) {
options.headers = {};
}
(options.headers as Record<string, string>)[key] = val;
}
});
}
options.method = method.toUpperCase(); // Ensure method is uppercase
this.log?.(
`⚡ Making ${method.toUpperCase()} request to "${url}" with options: ${JSON.stringify(options).slice(0, 100)}...`
);
const res = await this.page.evaluate(
async ([urlArg, optsArg]) => {
const response = await fetch(urlArg, optsArg);
// Return a serializable subset of the response
return {
status: response.status,
statusText: response.statusText,
// Correct way to get headers as a plain object for serialization
headers: (() => {
const result: Record<string, string> = {};
response.headers.forEach((value, key) => {
result[key] = value;
});
return result;
})(),
body: await response.text(),
};
},
[url, options] as [string, RequestInit] // Cast to ensure type compatibility
);
// Store the serializable response object
this.data.lastResponse = res;
this.log?.(
`✅ ${method.toUpperCase()} request to "${url}" completed. Status: ${res.status}. Body preview: ${res.body.slice(0, 100)}...`
);
}
When('I make a "{word}" request to {string}', When_I_make_HTTP_request_with_options);