playwright-cucumber-ts-steps 0.1.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +21 -11
  2. package/package.json +9 -2
  3. package/src/actions/clickSteps.ts +364 -142
  4. package/src/actions/cookieSteps.ts +66 -0
  5. package/src/actions/debugSteps.ts +17 -3
  6. package/src/actions/elementFindSteps.ts +822 -117
  7. package/src/actions/fillFormSteps.ts +234 -177
  8. package/src/actions/index.ts +12 -0
  9. package/src/actions/inputSteps.ts +318 -82
  10. package/src/actions/interceptionSteps.ts +295 -57
  11. package/src/actions/miscSteps.ts +984 -254
  12. package/src/actions/mouseSteps.ts +212 -55
  13. package/src/actions/scrollSteps.ts +114 -16
  14. package/src/actions/storageSteps.ts +267 -42
  15. package/src/assertions/buttonAndTextVisibilitySteps.ts +353 -95
  16. package/src/assertions/cookieSteps.ts +115 -36
  17. package/src/assertions/elementSteps.ts +414 -85
  18. package/src/assertions/formInputSteps.ts +375 -108
  19. package/src/assertions/index.ts +11 -0
  20. package/src/assertions/interceptionRequestsSteps.ts +619 -195
  21. package/src/assertions/locationSteps.ts +280 -64
  22. package/src/assertions/roleTestIdSteps.ts +244 -26
  23. package/src/assertions/semanticSteps.ts +257 -69
  24. package/src/assertions/storageSteps.ts +234 -73
  25. package/src/assertions/visualSteps.ts +245 -68
  26. package/src/custom_setups/loginHooks.ts +21 -2
  27. package/src/helpers/world.ts +30 -4
  28. package/src/index.ts +4 -25
  29. package/lib/actions/clickSteps.d.ts +0 -1
  30. package/lib/actions/clickSteps.js +0 -165
  31. package/lib/actions/cookieSteps.d.ts +0 -1
  32. package/lib/actions/cookieSteps.js +0 -28
  33. package/lib/actions/debugSteps.d.ts +0 -1
  34. package/lib/actions/debugSteps.js +0 -8
  35. package/lib/actions/elementFindSteps.d.ts +0 -1
  36. package/lib/actions/elementFindSteps.js +0 -217
  37. package/lib/actions/fillFormSteps.d.ts +0 -1
  38. package/lib/actions/fillFormSteps.js +0 -130
  39. package/lib/actions/inputSteps.d.ts +0 -1
  40. package/lib/actions/inputSteps.js +0 -97
  41. package/lib/actions/interceptionSteps.d.ts +0 -1
  42. package/lib/actions/interceptionSteps.js +0 -71
  43. package/lib/actions/miscSteps.d.ts +0 -1
  44. package/lib/actions/miscSteps.js +0 -320
  45. package/lib/actions/mouseSteps.d.ts +0 -1
  46. package/lib/actions/mouseSteps.js +0 -66
  47. package/lib/actions/scrollSteps.d.ts +0 -1
  48. package/lib/actions/scrollSteps.js +0 -23
  49. package/lib/actions/storageSteps.d.ts +0 -1
  50. package/lib/actions/storageSteps.js +0 -72
  51. package/lib/assertions/buttonAndTextVisibilitySteps.d.ts +0 -1
  52. package/lib/assertions/buttonAndTextVisibilitySteps.js +0 -150
  53. package/lib/assertions/cookieSteps.d.ts +0 -1
  54. package/lib/assertions/cookieSteps.js +0 -45
  55. package/lib/assertions/elementSteps.d.ts +0 -1
  56. package/lib/assertions/elementSteps.js +0 -90
  57. package/lib/assertions/formInputSteps.d.ts +0 -1
  58. package/lib/assertions/formInputSteps.js +0 -87
  59. package/lib/assertions/interceptionRequestsSteps.d.ts +0 -1
  60. package/lib/assertions/interceptionRequestsSteps.js +0 -201
  61. package/lib/assertions/locationSteps.d.ts +0 -1
  62. package/lib/assertions/locationSteps.js +0 -87
  63. package/lib/assertions/roleTestIdSteps.d.ts +0 -1
  64. package/lib/assertions/roleTestIdSteps.js +0 -26
  65. package/lib/assertions/semanticSteps.d.ts +0 -1
  66. package/lib/assertions/semanticSteps.js +0 -67
  67. package/lib/assertions/storageSteps.d.ts +0 -1
  68. package/lib/assertions/storageSteps.js +0 -74
  69. package/lib/assertions/visualSteps.d.ts +0 -1
  70. package/lib/assertions/visualSteps.js +0 -76
  71. package/lib/custom_setups/loginHooks.d.ts +0 -1
  72. package/lib/custom_setups/loginHooks.js +0 -113
  73. package/lib/helpers/checkPeerDeps.d.ts +0 -1
  74. package/lib/helpers/checkPeerDeps.js +0 -19
  75. package/lib/helpers/compareSnapshots.d.ts +0 -6
  76. package/lib/helpers/compareSnapshots.js +0 -20
  77. package/lib/helpers/hooks.d.ts +0 -1
  78. package/lib/helpers/hooks.js +0 -210
  79. package/lib/helpers/utils/fakerUtils.d.ts +0 -1
  80. package/lib/helpers/utils/fakerUtils.js +0 -60
  81. package/lib/helpers/utils/index.d.ts +0 -4
  82. package/lib/helpers/utils/index.js +0 -20
  83. package/lib/helpers/utils/optionsUtils.d.ts +0 -24
  84. package/lib/helpers/utils/optionsUtils.js +0 -88
  85. package/lib/helpers/utils/resolveUtils.d.ts +0 -6
  86. package/lib/helpers/utils/resolveUtils.js +0 -72
  87. package/lib/helpers/utils/sessionUtils.d.ts +0 -3
  88. package/lib/helpers/utils/sessionUtils.js +0 -40
  89. package/lib/helpers/world.d.ts +0 -31
  90. package/lib/helpers/world.js +0 -104
  91. package/lib/iframes/frames.d.ts +0 -1
  92. package/lib/iframes/frames.js +0 -11
  93. package/lib/index.d.ts +0 -28
  94. package/lib/index.js +0 -48
  95. package/lib/register.d.ts +0 -1
  96. package/lib/register.js +0 -6
@@ -1,216 +1,640 @@
1
1
  import { Then } from "@cucumber/cucumber";
2
2
  import { expect } from "@playwright/test";
3
3
  import type { CustomWorld } from "../helpers/world";
4
- // Accessing the Last Response
5
-
6
- Then("I should see response status {int}", function (expectedStatus: number) {
7
- expect(this.data.lastResponse?.status).toBe(expectedStatus);
8
- this.log(`Verified response status is ${expectedStatus}`);
9
- });
10
-
11
- Then("I should see response body contains {string}", function (expectedText: string) {
12
- expect(this.data.lastResponse?.body).toContain(expectedText);
13
- this.log(`Verified response body contains "${expectedText}"`);
14
- });
15
- Then("I see response body {string}", async function (this: CustomWorld, expected: string) {
16
- const res = this.data.lastResponse;
17
- const body = await res.text();
18
- if (body !== expected) throw new Error(`Expected body "${expected}", got "${body}"`);
19
- });
20
- Then("I see response body contains {string}", async function (this: CustomWorld, part: string) {
21
- const res = this.data.lastResponse;
22
- const body = await res.text();
23
- if (!body.includes(part)) throw new Error(`Body does not contain "${part}"`);
24
- });
25
- Then(
26
- "I see response body matches JSON schema {string}",
27
- async function (this: CustomWorld, schemaPath: string) {
28
- const res = this.data.lastResponse;
29
- const body = await res.text();
30
- const schema = require(schemaPath); // Assuming schema is a JSON file
31
- const Ajv = require("ajv");
32
- const ajv = new Ajv();
33
- const validate = ajv.compile(schema);
34
- const valid = validate(JSON.parse(body));
35
- if (!valid) {
36
- throw new Error(`Response body does not match schema: ${ajv.errorsText(validate.errors)}`);
37
- }
38
- }
39
- );
40
- Then(
41
- "I see response header {string} equals {string}",
42
- async function (this: CustomWorld, headerName: string, expectedValue: string) {
43
- const res = this.data.lastResponse;
44
- const headerValue = res.headers()[headerName.toLowerCase()];
45
- if (headerValue !== expectedValue) {
46
- throw new Error(
47
- `Expected header "${headerName}" to be "${expectedValue}", got "${headerValue}"`
48
- );
49
- }
50
- this.log(`Verified response header "${headerName}" equals "${expectedValue}"`);
51
- }
52
- );
53
- Then(
54
- "I see response header {string} contains {string}",
55
- async function (this: CustomWorld, headerName: string, expectedValue: string) {
56
- const res = this.data.lastResponse;
57
- const headerValue = res.headers()[headerName.toLowerCase()];
58
- if (!headerValue || !headerValue.includes(expectedValue)) {
59
- throw new Error(
60
- `Expected header "${headerName}" to contain "${expectedValue}", got "${headerValue}"`
61
- );
62
- }
63
- this.log(`Verified response header "${headerName}" contains "${expectedValue}"`);
4
+ // import Ajv from "ajv"; // Assuming Ajv is installed (npm install ajv)
5
+
6
+ // Define a type for the expected structure of lastResponse for consistency
7
+ interface LastResponse {
8
+ status: number;
9
+ statusText?: string;
10
+ headers: Record<string, string>; // Headers as a plain object for easy access
11
+ body: string; // Body content as a string
12
+ }
13
+
14
+ // Helper to ensure lastResponse is available and correctly typed
15
+ function getValidatedLastResponse(world: CustomWorld): LastResponse {
16
+ const response = world.data.lastResponse;
17
+ if (!response) {
18
+ throw new Error(
19
+ "No API response available in 'this.data.lastResponse'. Make sure a request step was executed."
20
+ );
64
21
  }
65
- );
66
- Then(
67
- "I see response header {string} does not contain {string}",
68
- async function (this: CustomWorld, headerName: string, unexpectedValue: string) {
69
- const res = this.data.lastResponse;
70
- const headerValue = res.headers()[headerName.toLowerCase()];
71
- if (headerValue && headerValue.includes(unexpectedValue)) {
72
- throw new Error(
73
- `Expected header "${headerName}" to not contain "${unexpectedValue}", but it does`
74
- );
75
- }
76
- this.log(`Verified response header "${headerName}" does not contain "${unexpectedValue}"`);
22
+ // Ensure the structure matches our expected LastResponse interface
23
+ if (
24
+ typeof response.status !== "number" ||
25
+ typeof response.body !== "string" ||
26
+ typeof response.headers !== "object"
27
+ ) {
28
+ throw new Error(
29
+ "Invalid 'lastResponse' structure. Expected properties: status (number), body (string), headers (object)."
30
+ );
77
31
  }
78
- );
79
- Then(
80
- "I see response header {string} does not equal {string}",
81
- async function (this: CustomWorld, headerName: string, unexpectedValue: string) {
82
- const res = this.data.lastResponse;
83
- const headerValue = res.headers()[headerName.toLowerCase()];
84
- if (headerValue === unexpectedValue) {
85
- throw new Error(
86
- `Expected header "${headerName}" to not equal "${unexpectedValue}", but it does`
87
- );
88
- }
89
- this.log(`Verified response header "${headerName}" does not equal "${unexpectedValue}"`);
32
+ return response as LastResponse;
33
+ }
34
+
35
+ // ===================================================================================
36
+ // ASSERTIONS: API RESPONSE STATUS
37
+ // ===================================================================================
38
+
39
+ /**
40
+ * Asserts that the status code of the last API response matches the expected number.
41
+ *
42
+ * ```gherkin
43
+ * Then I should see response status {int}
44
+ * ```
45
+ *
46
+ * @param expectedStatus - The expected HTTP status code (e.g., 200, 404).
47
+ *
48
+ * @example
49
+ * When I make request to "https://api.example.com/data"
50
+ * Then I should see response status 200
51
+ *
52
+ * @remarks
53
+ * This step requires a preceding step that stores the API response in
54
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
55
+ * It uses `expect().toBe()` for robust assertion.
56
+ * @category API Response Assertion Steps
57
+ */
58
+ export function Then_I_should_see_response_status(this: CustomWorld, expectedStatus: number) {
59
+ const res = getValidatedLastResponse(this);
60
+ expect(res.status).toBe(expectedStatus);
61
+ this.log?.(`✅ Verified response status is ${expectedStatus}.`);
62
+ }
63
+ Then("I should see response status {int}", Then_I_should_see_response_status);
64
+
65
+ /**
66
+ * Asserts that the status code of the last API response does NOT match the given number.
67
+ *
68
+ * ```gherkin
69
+ * Then I see response status is not {int}
70
+ * ```
71
+ *
72
+ * @param unexpectedStatus - The HTTP status code that is NOT expected.
73
+ *
74
+ * @example
75
+ * When I make request to "https://api.example.com/data"
76
+ * Then I see response status is not 404
77
+ *
78
+ * @remarks
79
+ * This step requires a preceding step that stores the API response in
80
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
81
+ * It uses `expect().not.toBe()` for robust assertion.
82
+ * @category API Response Assertion Steps
83
+ */
84
+ export function Then_I_see_response_status_is_not(this: CustomWorld, unexpectedStatus: number) {
85
+ const res = getValidatedLastResponse(this);
86
+ expect(res.status).not.toBe(unexpectedStatus);
87
+ this.log?.(`✅ Verified response status is NOT ${unexpectedStatus}.`);
88
+ }
89
+ Then("I see response status is not {int}", Then_I_see_response_status_is_not);
90
+
91
+ // ===================================================================================
92
+ // ASSERTIONS: API RESPONSE BODY
93
+ // ===================================================================================
94
+
95
+ /**
96
+ * Asserts that the body of the last API response contains the expected text substring.
97
+ *
98
+ * ```gherkin
99
+ * Then I should see response body contains {string}
100
+ * ```
101
+ *
102
+ * @param expectedText - The substring expected to be present in the response body.
103
+ *
104
+ * @example
105
+ * When I make request to "https://api.example.com/users"
106
+ * Then I should see response body contains "John Doe"
107
+ *
108
+ * @remarks
109
+ * This step requires a preceding step that stores the API response in
110
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
111
+ * It uses `expect().toContain()` for robust assertion.
112
+ * @category API Response Assertion Steps
113
+ */
114
+ export function Then_I_should_see_response_body_contains(this: CustomWorld, expectedText: string) {
115
+ const res = getValidatedLastResponse(this);
116
+ expect(res.body).toContain(expectedText);
117
+ this.log?.(`✅ Verified response body contains "${expectedText}".`);
118
+ }
119
+ // Note: You had two steps "I should see response body contains" and "I see response body contains".
120
+ // I've consolidated them to this single export function and will link both When patterns to it.
121
+ Then("I should see response body contains {string}", Then_I_should_see_response_body_contains);
122
+ Then("I see response body contains {string}", Then_I_should_see_response_body_contains);
123
+
124
+ /**
125
+ * Asserts that the body of the last API response strictly matches the expected string.
126
+ *
127
+ * ```gherkin
128
+ * Then I see response body {string}
129
+ * ```
130
+ *
131
+ * @param expectedBody - The exact string expected to be the response body.
132
+ *
133
+ * @example
134
+ * When I make request to "https://api.example.com/status"
135
+ * Then I see response body "OK"
136
+ *
137
+ * @remarks
138
+ * This step requires a preceding step that stores the API response in
139
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
140
+ * It performs a strict equality check on the entire response body string.
141
+ * @category API Response Assertion Steps
142
+ */
143
+ export function Then_I_see_response_body(this: CustomWorld, expectedBody: string) {
144
+ const res = getValidatedLastResponse(this);
145
+ expect(res.body).toBe(expectedBody);
146
+ this.log?.(`✅ Verified response body is exactly "${expectedBody}".`);
147
+ }
148
+ // Note: You had "I see response body {string}" and "I see response body matches {string}".
149
+ // I've consolidated them to this single export function and will link both Then patterns to it.
150
+ Then("I see response body {string}", Then_I_see_response_body);
151
+ Then("I see response body matches {string}", Then_I_see_response_body);
152
+
153
+ /**
154
+ * Asserts that the body of the last API response does NOT contain the given substring.
155
+ *
156
+ * ```gherkin
157
+ * Then I see response body does not contain {string}
158
+ * ```
159
+ *
160
+ * @param unexpectedPart - The substring expected NOT to be present in the response body.
161
+ *
162
+ * @example
163
+ * When I make request to "https://api.example.com/error"
164
+ * Then I see response body does not contain "internal server error"
165
+ *
166
+ * @remarks
167
+ * This step requires a preceding step that stores the API response in
168
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
169
+ * It checks for the absence of the substring within the response body.
170
+ * @category API Response Assertion Steps
171
+ */
172
+ export function Then_I_see_response_body_does_not_contain(
173
+ this: CustomWorld,
174
+ unexpectedPart: string
175
+ ) {
176
+ const res = getValidatedLastResponse(this);
177
+ expect(res.body).not.toContain(unexpectedPart);
178
+ this.log?.(`✅ Verified response body does NOT contain "${unexpectedPart}".`);
179
+ }
180
+ Then("I see response body does not contain {string}", Then_I_see_response_body_does_not_contain);
181
+
182
+ /**
183
+ * Asserts that the body of the last API response is empty (contains only whitespace or is empty).
184
+ *
185
+ * ```gherkin
186
+ * Then I see response body is empty
187
+ * ```
188
+ *
189
+ * @example
190
+ * When I make request to "https://api.example.com/no-content"
191
+ * Then I see response body is empty
192
+ *
193
+ * @remarks
194
+ * This step requires a preceding step that stores the API response in
195
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
196
+ * It trims whitespace and checks if the resulting string is empty.
197
+ * @category API Response Assertion Steps
198
+ */
199
+ export function Then_I_see_response_body_is_empty(this: CustomWorld) {
200
+ const res = getValidatedLastResponse(this);
201
+ expect(res.body.trim()).toBe("");
202
+ this.log?.(`✅ Verified response body is empty.`);
203
+ }
204
+ Then("I see response body is empty", Then_I_see_response_body_is_empty);
205
+
206
+ /**
207
+ * Asserts that the body of the last API response is NOT empty (contains non-whitespace characters).
208
+ *
209
+ * ```gherkin
210
+ * Then I see response body is not empty
211
+ * ```
212
+ *
213
+ * @example
214
+ * When I make request to "https://api.example.com/data"
215
+ * Then I see response body is not empty
216
+ *
217
+ * @remarks
218
+ * This step requires a preceding step that stores the API response in
219
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
220
+ * It trims whitespace and checks if the resulting string is not empty.
221
+ * @category API Response Assertion Steps
222
+ */
223
+ export function Then_I_see_response_body_is_not_empty(this: CustomWorld) {
224
+ const res = getValidatedLastResponse(this);
225
+ expect(res.body.trim()).not.toBe("");
226
+ this.log?.(`✅ Verified response body is not empty.`);
227
+ }
228
+ Then("I see response body is not empty", Then_I_see_response_body_is_not_empty);
229
+
230
+ /**
231
+ * Asserts that the body of the last API response is valid JSON.
232
+ *
233
+ * ```gherkin
234
+ * Then I see response body is JSON
235
+ * ```
236
+ *
237
+ * @example
238
+ * When I make request to "https://api.example.com/json-data"
239
+ * Then I see response body is JSON
240
+ *
241
+ * @remarks
242
+ * This step requires a preceding step that stores the API response in
243
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
244
+ * It attempts to parse the response body as JSON. If parsing fails, the step fails.
245
+ * @category API Response Assertion Steps
246
+ */
247
+ export function Then_I_see_response_body_is_JSON(this: CustomWorld) {
248
+ const res = getValidatedLastResponse(this);
249
+ try {
250
+ JSON.parse(res.body);
251
+ this.log?.(`✅ Verified response body is valid JSON.`);
252
+ } catch (e) {
253
+ const message = e instanceof Error ? e.message : String(e);
254
+ throw new Error(`Response body is not valid JSON: ${message}`);
90
255
  }
91
- );
92
- Then(
93
- "I see response header {string} exists",
94
- async function (this: CustomWorld, headerName: string) {
95
- const res = this.data.lastResponse;
96
- const headerValue = res.headers()[headerName.toLowerCase()];
97
- if (!headerValue) {
98
- throw new Error(`Expected header "${headerName}" to exist, but it does not`);
99
- }
100
- this.log(`Verified response header "${headerName}" exists`);
256
+ }
257
+ Then("I see response body is JSON", Then_I_see_response_body_is_JSON);
258
+
259
+ /**
260
+ * Asserts that the body of the last API response is NOT valid JSON.
261
+ *
262
+ * ```gherkin
263
+ * Then I see response body is not JSON
264
+ * ```
265
+ *
266
+ * @example
267
+ * When I make request to "https://api.example.com/plain-text"
268
+ * Then I see response body is not JSON
269
+ *
270
+ * @remarks
271
+ * This step requires a preceding step that stores the API response in
272
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
273
+ * It attempts to parse the response body as JSON and expects the parsing to fail.
274
+ * @category API Response Assertion Steps
275
+ */
276
+ export function Then_I_see_response_body_is_not_JSON(this: CustomWorld) {
277
+ const res = getValidatedLastResponse(this);
278
+ let isJson = true;
279
+ try {
280
+ JSON.parse(res.body);
281
+ } catch (e) {
282
+ isJson = false;
283
+ e instanceof Error ? e.message : String(e);
101
284
  }
102
- );
103
- Then(
104
- "I see response header {string} does not exist",
105
- async function (this: CustomWorld, headerName: string) {
106
- const res = this.data.lastResponse;
107
- const headerValue = res.headers()[headerName.toLowerCase()];
108
- if (headerValue) {
109
- throw new Error(`Expected header "${headerName}" to not exist, but it does`);
110
- }
111
- this.log(`Verified response header "${headerName}" does not exist`);
285
+ if (isJson) {
286
+ throw new Error(`Expected response body to not be JSON, but it is.`);
112
287
  }
113
- );
114
- Then("I see response status {int}", async function (this: CustomWorld, status: number) {
115
- const res = this.data.lastResponse;
116
- if (!res) throw new Error("No response available");
117
- const actual = res.status();
118
- if (actual !== status) throw new Error(`Expected status ${status}, got ${actual}`);
119
- });
120
- Then("I see response status is not {int}", async function (this: CustomWorld, status: number) {
121
- const res = this.data.lastResponse;
122
- if (!res) throw new Error("No response available");
123
- const actual = res.status();
124
- if (actual === status) throw new Error(`Expected status not to be ${status}, but it is`);
125
- });
126
- Then("I see response body matches JSON schema", async function (this: CustomWorld, schema: object) {
127
- const res = this.data.lastResponse;
128
- if (!res) throw new Error("No response available");
129
- const body = await res.text();
130
- const Ajv = require("ajv");
288
+ this.log?.(`✅ Verified response body is NOT JSON.`);
289
+ }
290
+ Then("I see response body is not JSON", Then_I_see_response_body_is_not_JSON);
291
+
292
+ /**
293
+ * Asserts that the body of the last API response matches the given JSON schema.
294
+ *
295
+ * ```gherkin
296
+ * Then I see response body matches JSON schema {string}
297
+ * ```
298
+ *
299
+ * @param schemaPath - The path to the JSON schema file (e.g., "schemas/responseSchema.js").
300
+ *
301
+ * @example
302
+ * When I make request to "https://api.example.com/users/1"
303
+ * Then I see response body matches JSON schema "schemas/userSchema.js"
304
+ *
305
+ * @remarks
306
+ * This step requires a preceding step that stores the API response in
307
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
308
+ * It dynamically imports the schema file and uses `ajv` to validate the JSON response body.
309
+ * Ensure `ajv` is installed (`npm install ajv`) and your schema files are accessible.
310
+ * @category API Response Assertion Steps
311
+ */
312
+ export async function Then_I_see_response_body_matches_JSON_schema_path(
313
+ this: CustomWorld,
314
+ schemaPath: string
315
+ ) {
316
+ const res = getValidatedLastResponse(this);
317
+ const body = res.body; // Body is already a string from getValidatedLastResponse
318
+
319
+ // Dynamically import Ajv and the schema
320
+ const AjvModule = await import("ajv");
321
+ const Ajv = AjvModule.default || AjvModule; // Handle potential default export differences
131
322
  const ajv = new Ajv();
323
+
324
+ // Dynamically import the schema, handling both CommonJS and ES Module exports
325
+ let schema: object;
326
+ try {
327
+ const schemaModule = await import(schemaPath);
328
+ schema = schemaModule.default || schemaModule;
329
+ } catch (error) {
330
+ throw new Error(`Failed to load JSON schema from "${schemaPath}": ${(error as Error).message}`);
331
+ }
332
+
132
333
  const validate = ajv.compile(schema);
133
- const valid = validate(JSON.parse(body));
334
+ let parsedBody: any;
335
+ try {
336
+ parsedBody = JSON.parse(body);
337
+ } catch (e) {
338
+ throw new Error(
339
+ `Response body is not valid JSON and cannot be validated against schema: ${(e as Error).message}`
340
+ );
341
+ }
342
+
343
+ const valid = validate(parsedBody);
134
344
  if (!valid) {
135
345
  throw new Error(`Response body does not match schema: ${ajv.errorsText(validate.errors)}`);
136
346
  }
137
- this.log(`Response body matches JSON schema`);
138
- });
139
- Then("I see response body is empty", async function (this: CustomWorld) {
140
- const res = this.data.lastResponse;
141
- if (!res) throw new Error("No response available");
142
- const body = await res.text();
143
- if (body.trim() !== "") {
144
- throw new Error(`Expected empty response body, got "${body}"`);
145
- }
146
- this.log(`Verified response body is empty`);
147
- });
148
- Then("I see response body is not empty", async function (this: CustomWorld) {
149
- const res = this.data.lastResponse;
150
- if (!res) throw new Error("No response available");
151
- const body = await res.text();
152
- if (body.trim() === "") {
153
- throw new Error(`Expected non-empty response body, got empty`);
154
- }
155
- this.log(`Verified response body is not empty`);
156
- });
157
- Then("I see response body matches {string}", async function (this: CustomWorld, expected: string) {
158
- const res = this.data.lastResponse;
159
- if (!res) throw new Error("No response available");
160
- const body = await res.text();
161
- if (body !== expected) {
162
- throw new Error(`Expected body "${expected}", got "${body}"`);
163
- }
164
- this.log(`Verified response body matches: ${expected}`);
165
- });
166
- Then("I see response body contains {string}", async function (this: CustomWorld, part: string) {
167
- const res = this.data.lastResponse;
168
- if (!res) throw new Error("No response available");
169
- const body = await res.text();
170
- if (!body.includes(part)) {
171
- throw new Error(`Body does not contain "${part}"`);
172
- }
173
- this.log(`Verified response body contains: ${part}`);
174
- });
347
+ this.log?.(`✅ Verified response body matches JSON schema from "${schemaPath}".`);
348
+ }
175
349
  Then(
176
- "I see response body does not contain {string}",
177
- async function (this: CustomWorld, part: string) {
178
- const res = this.data.lastResponse;
179
- if (!res) throw new Error("No response available");
180
- const body = await res.text();
181
- if (body.includes(part)) {
182
- throw new Error(`Body contains "${part}", but it should not`);
183
- }
184
- this.log(`Verified response body does not contain: ${part}`);
185
- }
350
+ "I see response body matches JSON schema {string}",
351
+ Then_I_see_response_body_matches_JSON_schema_path
186
352
  );
187
- Then("I see response body is JSON", async function (this: CustomWorld) {
188
- const res = this.data.lastResponse;
189
- if (!res) throw new Error("No response available");
190
- const body = await res.text();
353
+
354
+ /**
355
+ * Asserts that the body of the last API response matches the given JSON schema object directly provided in the step.
356
+ *
357
+ * ```gherkin
358
+ * Then I see response body matches JSON schema:
359
+ * """
360
+ * { "type": "object", "properties": { "id": { "type": "number" } } }
361
+ * """
362
+ * ```
363
+ *
364
+ * @param schema - The JSON schema object (provided as a Cucumber DocString).
365
+ *
366
+ * @example
367
+ * When I make request to "https://api.example.com/users/latest"
368
+ * Then I see response body matches JSON schema:
369
+ * """
370
+ * {
371
+ * "type": "object",
372
+ * "properties": {
373
+ * "id": { "type": "number" },
374
+ * "name": { "type": "string" }
375
+ * },
376
+ * "required": ["id", "name"]
377
+ * }
378
+ * """
379
+ *
380
+ * @remarks
381
+ * This step requires a preceding step that stores the API response in
382
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
383
+ * It expects the schema as a direct JSON string (DocString) in the Gherkin step.
384
+ * Uses `ajv` to validate the JSON response body against this inline schema.
385
+ * Ensure `ajv` is installed (`npm install ajv`).
386
+ * @category API Response Assertion Steps
387
+ */
388
+ export async function Then_I_see_response_body_matches_JSON_schema_object(
389
+ this: CustomWorld,
390
+ schemaString: string
391
+ ) {
392
+ const res = getValidatedLastResponse(this);
393
+ const body = res.body;
394
+
395
+ const AjvModule = await import("ajv");
396
+ const Ajv = AjvModule.default || AjvModule;
397
+ const ajv = new Ajv();
398
+
399
+ let schema: object;
191
400
  try {
192
- JSON.parse(body);
193
- this.log(`Verified response body is valid JSON`);
401
+ schema = JSON.parse(schemaString);
194
402
  } catch (e) {
195
- if (e instanceof Error) {
196
- throw new Error(`Response body is not valid JSON: ${e.message}`);
197
- } else {
198
- throw new Error(`Response body is not valid JSON: ${String(e)}`);
199
- }
403
+ throw new Error(
404
+ `Failed to parse inline JSON schema: ${(e as Error).message}. Ensure it is valid JSON.`
405
+ );
200
406
  }
201
- });
202
- Then("I see response body is not JSON", async function (this: CustomWorld) {
203
- const res = this.data.lastResponse;
204
- if (!res) throw new Error("No response available");
205
- const body = await res.text();
407
+
408
+ const validate = ajv.compile(schema);
409
+ let parsedBody: any;
206
410
  try {
207
- JSON.parse(body);
208
- throw new Error(`Expected response body to not be JSON, but it is`);
411
+ parsedBody = JSON.parse(body);
209
412
  } catch (e) {
210
- if (e instanceof Error) {
211
- this.log(`Verified response body is not JSON: ${e.message}`);
212
- } else {
213
- this.log(`Verified response body is not JSON: ${String(e)}`);
214
- }
413
+ throw new Error(
414
+ `Response body is not valid JSON and cannot be validated against inline schema: ${(e as Error).message}`
415
+ );
215
416
  }
216
- });
417
+
418
+ const valid = validate(parsedBody);
419
+ if (!valid) {
420
+ throw new Error(
421
+ `Response body does not match inline schema: ${ajv.errorsText(validate.errors)}`
422
+ );
423
+ }
424
+ this.log?.(`✅ Verified response body matches inline JSON schema.`);
425
+ }
426
+ Then(
427
+ "I see response body matches JSON schema",
428
+ Then_I_see_response_body_matches_JSON_schema_object
429
+ );
430
+
431
+ // ===================================================================================
432
+ // ASSERTIONS: API RESPONSE HEADERS
433
+ // ===================================================================================
434
+
435
+ /**
436
+ * Asserts that a specific header in the last API response strictly equals an expected value.
437
+ *
438
+ * ```gherkin
439
+ * Then I see response header {string} equals {string}
440
+ * ```
441
+ *
442
+ * @param headerName - The name of the header (case-insensitive, e.g., "Content-Type").
443
+ * @param expectedValue - The exact expected value of the header.
444
+ *
445
+ * @example
446
+ * When I make request to "https://api.example.com/data"
447
+ * Then I see response header "content-type" equals "application/json"
448
+ *
449
+ * @remarks
450
+ * This step requires a preceding step that stores the API response in
451
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
452
+ * It retrieves the header value and performs a strict equality check.
453
+ * @category API Response Assertion Steps
454
+ */
455
+ export function Then_I_see_response_header_equals(
456
+ this: CustomWorld,
457
+ headerName: string,
458
+ expectedValue: string
459
+ ) {
460
+ const res = getValidatedLastResponse(this);
461
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
462
+ expect(
463
+ headerValue,
464
+ `Expected header "${headerName}" to be "${expectedValue}", but got "${headerValue}".`
465
+ ).toBe(expectedValue);
466
+ this.log?.(`✅ Verified response header "${headerName}" equals "${expectedValue}".`);
467
+ }
468
+ Then("I see response header {string} equals {string}", Then_I_see_response_header_equals);
469
+
470
+ /**
471
+ * Asserts that a specific header in the last API response contains a given substring.
472
+ *
473
+ * ```gherkin
474
+ * Then I see response header {string} contains {string}
475
+ * ```
476
+ *
477
+ * @param headerName - The name of the header (case-insensitive, e.g., "Set-Cookie").
478
+ * @param expectedPart - The substring expected to be contained within the header's value.
479
+ *
480
+ * @example
481
+ * When I make request to "https://api.example.com/login"
482
+ * Then I see response header "set-cookie" contains "session_id"
483
+ *
484
+ * @remarks
485
+ * This step requires a preceding step that stores the API response in
486
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
487
+ * It retrieves the header value and checks if it includes the `expectedPart`.
488
+ * @category API Response Assertion Steps
489
+ */
490
+ export function Then_I_see_response_header_contains(
491
+ this: CustomWorld,
492
+ headerName: string,
493
+ expectedPart: string
494
+ ) {
495
+ const res = getValidatedLastResponse(this);
496
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
497
+ expect(
498
+ headerValue,
499
+ `Header "${headerName}" not found or is empty. Expected to contain "${expectedPart}".`
500
+ ).toBeDefined(); // Ensure header exists before checking content
501
+ expect(
502
+ headerValue,
503
+ `Expected header "${headerName}" to contain "${expectedPart}", but got "${headerValue}".`
504
+ ).toContain(expectedPart);
505
+ this.log?.(`✅ Verified response header "${headerName}" contains "${expectedPart}".`);
506
+ }
507
+ Then("I see response header {string} contains {string}", Then_I_see_response_header_contains);
508
+
509
+ /**
510
+ * Asserts that a specific header in the last API response does NOT contain a given substring.
511
+ *
512
+ * ```gherkin
513
+ * Then I see response header {string} does not contain {string}
514
+ * ```
515
+ *
516
+ * @param headerName - The name of the header (case-insensitive).
517
+ * @param unexpectedPart - The substring expected NOT to be present within the header's value.
518
+ *
519
+ * @example
520
+ * When I make request to "https://api.example.com/no-cache"
521
+ * Then I see response header "cache-control" does not contain "no-store"
522
+ *
523
+ * @remarks
524
+ * This step requires a preceding step that stores the API response in
525
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
526
+ * It retrieves the header value and asserts that it does not include the `unexpectedPart`.
527
+ * @category API Response Assertion Steps
528
+ */
529
+ export function Then_I_see_response_header_does_not_contain(
530
+ this: CustomWorld,
531
+ headerName: string,
532
+ unexpectedPart: string
533
+ ) {
534
+ const res = getValidatedLastResponse(this);
535
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
536
+ if (headerValue !== undefined) {
537
+ // Only check if header exists
538
+ expect(
539
+ headerValue,
540
+ `Expected header "${headerName}" to NOT contain "${unexpectedPart}", but it does.`
541
+ ).not.toContain(unexpectedPart);
542
+ }
543
+ this.log?.(`✅ Verified response header "${headerName}" does NOT contain "${unexpectedPart}".`);
544
+ }
545
+ Then(
546
+ "I see response header {string} does not contain {string}",
547
+ Then_I_see_response_header_does_not_contain
548
+ );
549
+
550
+ /**
551
+ * Asserts that a specific header in the last API response does NOT strictly equal a given value.
552
+ *
553
+ * ```gherkin
554
+ * Then I see response header {string} does not equal {string}
555
+ * ```
556
+ *
557
+ * @param headerName - The name of the header (case-insensitive).
558
+ * @param unexpectedValue - The value that is NOT expected for the header.
559
+ *
560
+ * @example
561
+ * When I make request to "https://api.example.com/html-page"
562
+ * Then I see response header "content-type" does not equal "application/json"
563
+ *
564
+ * @remarks
565
+ * This step requires a preceding step that stores the API response in
566
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
567
+ * It retrieves the header value and asserts that it does not strictly match `unexpectedValue`.
568
+ * @category API Response Assertion Steps
569
+ */
570
+ export function Then_I_see_response_header_does_not_equal(
571
+ this: CustomWorld,
572
+ headerName: string,
573
+ unexpectedValue: string
574
+ ) {
575
+ const res = getValidatedLastResponse(this);
576
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
577
+ expect(
578
+ headerValue,
579
+ `Expected header "${headerName}" to NOT be "${unexpectedValue}", but it is.`
580
+ ).not.toBe(unexpectedValue);
581
+ this.log?.(`✅ Verified response header "${headerName}" does NOT equal "${unexpectedValue}".`);
582
+ }
583
+ Then(
584
+ "I see response header {string} does not equal {string}",
585
+ Then_I_see_response_header_does_not_equal
586
+ );
587
+
588
+ /**
589
+ * Asserts that a specific header in the last API response exists (is present).
590
+ *
591
+ * ```gherkin
592
+ * Then I see response header {string} exists
593
+ * ```
594
+ *
595
+ * @param headerName - The name of the header expected to exist.
596
+ *
597
+ * @example
598
+ * When I make request to "https://api.example.com/data"
599
+ * Then I see response header "content-length" exists
600
+ *
601
+ * @remarks
602
+ * This step requires a preceding step that stores the API response in
603
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
604
+ * It checks if the header value is defined (not `undefined` or `null`).
605
+ * @category API Response Assertion Steps
606
+ */
607
+ export function Then_I_see_response_header_exists(this: CustomWorld, headerName: string) {
608
+ const res = getValidatedLastResponse(this);
609
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
610
+ expect(headerValue, `Header "${headerName}" should exist but was not found.`).toBeDefined();
611
+ this.log?.(`✅ Verified response header "${headerName}" exists.`);
612
+ }
613
+ Then("I see response header {string} exists", Then_I_see_response_header_exists);
614
+
615
+ /**
616
+ * Asserts that a specific header in the last API response does NOT exist (is not present).
617
+ *
618
+ * ```gherkin
619
+ * Then I see response header {string} does not exist
620
+ * ```
621
+ *
622
+ * @param headerName - The name of the header expected NOT to exist.
623
+ *
624
+ * @example
625
+ * When I make request to "https://api.example.com/simple"
626
+ * Then I see response header "x-powered-by" does not exist
627
+ *
628
+ * @remarks
629
+ * This step requires a preceding step that stores the API response in
630
+ * {@link CustomWorld.data.lastResponse | this.data.lastResponse}.
631
+ * It checks if the header value is `undefined` or `null`.
632
+ * @category API Response Assertion Steps
633
+ */
634
+ export function Then_I_see_response_header_does_not_exist(this: CustomWorld, headerName: string) {
635
+ const res = getValidatedLastResponse(this);
636
+ const headerValue = res.headers[headerName.toLowerCase()]; // Access from plain object
637
+ expect(headerValue, `Header "${headerName}" should NOT exist but was found.`).toBeUndefined();
638
+ this.log?.(`✅ Verified response header "${headerName}" does NOT exist.`);
639
+ }
640
+ Then("I see response header {string} does not exist", Then_I_see_response_header_does_not_exist);