playwright-cucumber-ts-steps 1.0.2 → 1.0.3

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.
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
6
6
  [![Playwright](https://img.shields.io/badge/tested%20with-Playwright-blueviolet?style=flat-square&logo=playwright)](https://playwright.dev)
7
7
  [![TypeScript](https://img.shields.io/badge/written%20in-TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
8
+ [![GitHub stars](https://img.shields.io/github/stars/qaPaschalE/playwright-cucumber-ts-steps)](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/stargazers)
8
9
 
9
10
  **The "Low-Code" BDD Framework for Playwright.**
10
11
 
@@ -193,6 +194,92 @@ Feature: Admin Panel
193
194
 
194
195
  ```
195
196
 
197
+ ### 4. Data Tables (Forms)
198
+
199
+ Fill out entire forms in a single step using a Data Table. You can type, click, check, or assert visibility in one go.
200
+
201
+ ```gherkin
202
+ Scenario: Registration
203
+ When I fill the following "Registration" form data:
204
+ | #first-name | John |
205
+ | #last-name | Doe |
206
+ | #email | john@test.com |
207
+ | #newsletter | check |
208
+ | #submit-btn | click |
209
+ | .success | assert:visible |
210
+ ```
211
+
212
+ ### 5. API Testing (Tables & Files)
213
+
214
+ Validate your backend directly. You can send payloads via Tables or JSON Files.
215
+
216
+ #### Option A: Data Table Payload
217
+
218
+ ```gherkin
219
+ Scenario: Create User (Table)
220
+ When I make a POST request to "[https://reqres.in/api/users](https://reqres.in/api/users)" with data:
221
+ | name | Neo |
222
+ | job | The Chosen |
223
+ Then I expect the response status to be 201
224
+ And I expect the response property "name" to be "Neo"
225
+ ```
226
+
227
+ #### Option B: File Payload
228
+
229
+ ```gherkin
230
+ Scenario: Create User (File)
231
+ # Reads from 'data/user.json' in your project root
232
+ When I make a POST request to "/api/users" with payload from "data/user.json"
233
+ Then I expect the response status to be 201
234
+ ```
235
+
236
+ ### 6. Network Mocking
237
+
238
+ Simulate backend responses to test UI behavior without relying on real APIs.
239
+
240
+ ```gherkin
241
+ Scenario: Mocking User Profile
242
+ # Intercept calls to /api/user/1 and return fake data
243
+ Given I mock the API endpoint "*/**/api/user/1" with body '{"name": "Mocked User"}'
244
+
245
+ # When the UI calls the API, it gets our fake data
246
+ When I visit "/profile"
247
+ Then I expect "#username-display" to have text "Mocked User"
248
+
249
+ ```
250
+
251
+ ### 7. Database Testing (Adapter Pattern)
252
+
253
+ You can validate database states by injecting your own DB driver into the runner.
254
+
255
+ **1. In your `bdd.spec.ts`:**
256
+
257
+ ```typescript
258
+ import { runTests } from "playwright-cucumber-ts-steps";
259
+ import pg from "pg"; // Your driver (pg, mysql, mongo, etc)
260
+
261
+ // wrapper function
262
+ const queryDb = async (query: string) => {
263
+ const client = new pg.Client(process.env.DB_URL);
264
+ await client.connect();
265
+ const res = await client.query(query);
266
+ await client.end();
267
+ return res.rows; // Must return an array of objects
268
+ };
269
+
270
+ runTests("features/*.feature", { dbQuery: queryDb });
271
+ ```
272
+
273
+ **2. In your `Feature file`:**
274
+
275
+ ```gherkin
276
+ Scenario: Create User
277
+ When I run the database query "INSERT INTO users (name) VALUES ('Bob')"
278
+ Then I expect the database to return 1 record
279
+ And I expect the first database record to contain:
280
+ | name | Bob |
281
+ ```
282
+
196
283
  ---
197
284
 
198
285
  ## 📖 Step Glossary (Cheat Sheet)
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const registry_1 = require("../../core/registry");
4
+ const test_1 = require("@playwright/test");
5
+ // CHANGE: Removed the ':' at the end of the string below
6
+ (0, registry_1.Step)("I fill the following {string} form data", async (page, formName, tableData) => {
7
+ console.log(`📝 Processing Form: ${formName}`);
8
+ // The runner passes the table data as the last argument
9
+ // tableData = [ ['#username', 'tomsmith'], ['#password', '...'] ]
10
+ // Guard clause: Ensure tableData exists to prevent crashes if user forgets the table
11
+ if (!tableData || !Array.isArray(tableData)) {
12
+ throw new Error(`❌ The step "I fill the following '${formName}' form data" requires a Data Table below it.`);
13
+ }
14
+ for (const row of tableData) {
15
+ const selector = row[0];
16
+ const value = row[1];
17
+ if (value === "click") {
18
+ await page.click(selector);
19
+ }
20
+ else if (value === "check") {
21
+ await page.check(selector);
22
+ }
23
+ else if (value === "assert:visible") {
24
+ await (0, test_1.expect)(page.locator(selector)).toBeVisible();
25
+ }
26
+ else if (value.startsWith("assert:text:")) {
27
+ const text = value.replace("assert:text:", "");
28
+ await (0, test_1.expect)(page.locator(selector)).toHaveText(text);
29
+ }
30
+ else {
31
+ await page.fill(selector, value);
32
+ }
33
+ }
34
+ });
@@ -2,3 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  require("./navigation");
4
4
  require("./interactions");
5
+ require("./formTable");
@@ -2,3 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  require("./requests");
4
4
  require("./assertions");
5
+ require("./mock");
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const registry_1 = require("../../core/registry");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ // 1. Mock with Inline JSON
40
+ // Usage: Given I mock the API endpoint "/api/users" with body '{"id": 1, "name": "Fake"}'
41
+ (0, registry_1.Step)("I mock the API endpoint {string} with body {string}", async (page, urlPattern, jsonBody) => {
42
+ await page.route(urlPattern, async (route) => {
43
+ const json = JSON.parse(jsonBody);
44
+ await route.fulfill({
45
+ status: 200,
46
+ contentType: "application/json",
47
+ body: JSON.stringify(json),
48
+ });
49
+ });
50
+ console.log(`🎭 Mocked ${urlPattern} with inline JSON`);
51
+ });
52
+ // 2. Mock with File
53
+ // Usage: Given I mock the API endpoint "/api/users" with response from "mocks/users.json"
54
+ (0, registry_1.Step)("I mock the API endpoint {string} with response from {string}", async (page, urlPattern, filePath) => {
55
+ const fullPath = path.resolve(process.cwd(), filePath);
56
+ if (!fs.existsSync(fullPath)) {
57
+ throw new Error(`❌ Mock file not found at: ${fullPath}`);
58
+ }
59
+ const bodyContent = fs.readFileSync(fullPath, "utf8");
60
+ await page.route(urlPattern, async (route) => {
61
+ await route.fulfill({
62
+ status: 200,
63
+ contentType: "application/json",
64
+ body: bodyContent,
65
+ });
66
+ });
67
+ console.log(`🎭 Mocked ${urlPattern} with file: ${filePath}`);
68
+ });
69
+ // 3. Mock Status Code Only (Simulate Errors)
70
+ // Usage: Given I mock the API endpoint "/api/broken" with status 500
71
+ (0, registry_1.Step)("I mock the API endpoint {string} with status {int}", async (page, urlPattern, statusCode) => {
72
+ await page.route(urlPattern, async (route) => {
73
+ await route.fulfill({ status: statusCode });
74
+ });
75
+ });
@@ -1,7 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  const registry_1 = require("../../core/registry");
4
37
  const state_1 = require("./state");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
5
40
  // GET Request
6
41
  (0, registry_1.Step)("I make a GET request to {string}", async (page, url) => {
7
42
  const response = await page.request.get(url);
@@ -13,11 +48,38 @@ const state_1 = require("./state");
13
48
  const response = await page.request.delete(url);
14
49
  state_1.apiState.setResponse(response);
15
50
  });
16
- // POST Request with JSON Body
17
- // Usage: I make a POST request to "/api/login" with body '{"user": "admin"}'
18
- (0, registry_1.Step)("I make a POST request to {string} with body {string}", async (page, url, bodyString) => {
51
+ // 1. POST with Data Table
52
+ // Usage:
53
+ // When I make a POST request to "/api/users" with data:
54
+ // | name | John |
55
+ // | job | Dev |
56
+ (0, registry_1.Step)("I make a POST request to {string} with data", async (page, url, tableData) => {
57
+ if (!tableData)
58
+ throw new Error("This step requires a Data Table.");
59
+ // Convert Table [ ["key", "val"], ["k2", "v2"] ] -> Object { key: "val", k2: "v2" }
60
+ const payload = tableData.reduce((acc, row) => {
61
+ acc[row[0]] = row[1];
62
+ return acc;
63
+ }, {});
64
+ const response = await page.request.post(url, {
65
+ data: payload,
66
+ headers: { "Content-Type": "application/json" },
67
+ });
68
+ state_1.apiState.setResponse(response);
69
+ });
70
+ // 2. POST with File Payload
71
+ // Usage: When I make a POST request to "/api/users" with payload from "data/user.json"
72
+ (0, registry_1.Step)("I make a POST request to {string} with payload from {string}", async (page, url, filePath) => {
73
+ // Resolve file path (relative to root)
74
+ const fullPath = path.resolve(process.cwd(), filePath);
75
+ if (!fs.existsSync(fullPath)) {
76
+ throw new Error(`❌ Payload file not found at: ${fullPath}`);
77
+ }
78
+ // Read and parse JSON
79
+ const fileContent = fs.readFileSync(fullPath, "utf8");
80
+ const payload = JSON.parse(fileContent);
19
81
  const response = await page.request.post(url, {
20
- data: JSON.parse(bodyString), // Parse the string input into a JSON object
82
+ data: payload,
21
83
  headers: { "Content-Type": "application/json" },
22
84
  });
23
85
  state_1.apiState.setResponse(response);
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbState = void 0;
4
+ require("./steps");
5
+ var state_1 = require("./state");
6
+ Object.defineProperty(exports, "dbState", { enumerable: true, get: function () { return state_1.dbState; } });
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dbState = void 0;
4
+ // Holds the user's custom DB function
5
+ let dbAdapter = null;
6
+ let lastResult = null;
7
+ exports.dbState = {
8
+ // Runner calls this to register the user's function
9
+ setAdapter: (fn) => {
10
+ dbAdapter = fn;
11
+ },
12
+ // Step calls this to run a query
13
+ executeQuery: async (query) => {
14
+ if (!dbAdapter) {
15
+ throw new Error("❌ No Database Adapter found. Pass a 'dbQuery' function to runTests().");
16
+ }
17
+ const result = await dbAdapter(query);
18
+ lastResult = result;
19
+ console.log(`🗄️ DB Result:`, JSON.stringify(lastResult));
20
+ return result;
21
+ },
22
+ // Assertions use this to check results
23
+ getLastResult: () => lastResult,
24
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const registry_1 = require("../../core/registry");
4
+ const state_1 = require("./state");
5
+ const test_1 = require("@playwright/test");
6
+ // 1. Run Query
7
+ (0, registry_1.Step)("I run the database query {string}", async (page, query) => {
8
+ await state_1.dbState.executeQuery(query);
9
+ });
10
+ // 2. Count Check
11
+ (0, registry_1.Step)("I expect the database to return {int} record(s)", async (page, count) => {
12
+ const result = state_1.dbState.getLastResult();
13
+ if (Array.isArray(result)) {
14
+ (0, test_1.expect)(result.length).toBe(count);
15
+ }
16
+ else {
17
+ throw new Error(`Expected array result but got: ${typeof result}`);
18
+ }
19
+ });
20
+ // 3. JSON/Table Check
21
+ // CHANGE: Removed the colon ':' at the end of the string below
22
+ (0, registry_1.Step)("I expect the first database record to contain", async (page, tableData) => {
23
+ const result = state_1.dbState.getLastResult();
24
+ // Guard Clauses
25
+ if (!Array.isArray(result) || result.length === 0) {
26
+ throw new Error("Database returned no records to check.");
27
+ }
28
+ if (!tableData) {
29
+ throw new Error("This step requires a Data Table.");
30
+ }
31
+ const firstRow = result[0]; // Check the first record found
32
+ // tableData is [ ["name", "Alice"], ["email", "alice@db.com"] ]
33
+ for (const row of tableData) {
34
+ const key = row[0];
35
+ const expectedValue = row[1];
36
+ // Check if the key exists in the DB result
37
+ if (!(key in firstRow)) {
38
+ throw new Error(`DB Record does not have column: ${key}`);
39
+ }
40
+ // Loose equality check (DB might return int, Gherkin sends string)
41
+ (0, test_1.expect)(String(firstRow[key])).toBe(expectedValue);
42
+ }
43
+ });
@@ -45,19 +45,17 @@ require("../backend/assertions/index");
45
45
  require("../backend/elements/index");
46
46
  require("../backend/api/index");
47
47
  require("../backend/auth/index");
48
+ const state_1 = require("../backend/db/state");
49
+ require("../backend/db/index"); // Register DB steps
48
50
  /**
49
51
  * The main test runner. Parses feature files and executes them as Playwright tests.
50
- * * @param featureGlob - Glob pattern to find feature files (e.g., 'features/*.feature')
51
- * @param options - Configuration options for filtering tags
52
- * * @example
53
- * ```ts
54
- * // Run all features
55
- * runTests('features/*.feature');
56
- * * // Run only @smoke tests
57
- * runTests('features/*.feature', { tags: '@smoke' });
58
- * ```
52
+ * Includes support for Data Tables and Auto-Screenshots.
59
53
  */
60
54
  function runTests(featureGlob, options) {
55
+ // 1. Register DB Adapter if provided
56
+ if (options?.dbQuery) {
57
+ state_1.dbState.setAdapter(options.dbQuery);
58
+ }
61
59
  const files = (0, glob_1.globSync)(featureGlob);
62
60
  for (const file of files) {
63
61
  const content = fs.readFileSync(file, "utf8");
@@ -70,53 +68,70 @@ function runTests(featureGlob, options) {
70
68
  let match;
71
69
  while ((match = scenarioRegex.exec(content)) !== null) {
72
70
  // 1. CAPTURE DATA IMMEDIATELY
73
- // We calculate everything here so we don't rely on 'match' later
74
71
  const foundTags = match[1] || "";
75
72
  const scenarioName = match[2].trim();
76
73
  const startIndex = match.index + match[0].length;
77
74
  const nextMatchIndex = content.slice(startIndex).search(/Scenario:/);
78
75
  const blockEnd = nextMatchIndex === -1 ? content.length : startIndex + nextMatchIndex;
79
- // This variable 'scenarioBlock' is now safe to use inside the test
80
76
  const scenarioBlock = content.slice(startIndex, blockEnd);
81
77
  if (options?.tags && !foundTags.includes(options.tags)) {
82
78
  continue;
83
79
  }
84
80
  (0, test_1.test)(scenarioName, async ({ page }, testInfo) => {
85
- // 1. Calculate the scenario block (Same as before)
86
81
  const lines = scenarioBlock
87
82
  .trim()
88
83
  .split("\n")
89
84
  .map((l) => l.trim())
90
85
  .filter((l) => l);
91
- for (const stepText of lines) {
86
+ // CHANGED: Use index loop to handle Data Tables (look-ahead)
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const stepText = lines[i];
92
89
  if (stepText.startsWith("#") ||
93
90
  stepText.startsWith("@") ||
94
91
  stepText === "")
95
92
  continue;
96
- const cleanStep = stepText.replace(/^(Given|When|Then|And|But)\s+/i, "");
93
+ // 2. CHECK FOR DATA TABLE
94
+ // We look at the NEXT lines to see if they start with a pipe '|'
95
+ const tableData = [];
96
+ while (i + 1 < lines.length && lines[i + 1].startsWith("|")) {
97
+ i++; // Move the index forward so we don't process this line as a step
98
+ const row = lines[i]
99
+ .split("|")
100
+ .map((cell) => cell.trim())
101
+ .filter((cell) => cell !== ""); // Remove empty start/end strings from split
102
+ tableData.push(row);
103
+ }
104
+ // 3. CLEAN STEP TEXT
105
+ // Remove "Given/When/..." and trailing colons ":"
106
+ const cleanStep = stepText
107
+ .replace(/^(Given|When|Then|And|But)\s+/i, "")
108
+ .replace(/:$/, "")
109
+ .trim();
97
110
  const stepDef = findMatchingStep(cleanStep);
98
111
  if (!stepDef) {
99
112
  throw new Error(`❌ Undefined Step: "${cleanStep}"`);
100
113
  }
101
- // 2. ERROR HANDLING WRAPPER
114
+ // 4. EXECUTE WITH ERROR HANDLING
102
115
  try {
103
- // Execute the step
104
- await stepDef.fn(page, ...stepDef.args);
116
+ // Construct arguments: Regex args + Table data (if exists)
117
+ const args = [...stepDef.args];
118
+ if (tableData.length > 0) {
119
+ args.push(tableData);
120
+ }
121
+ // Pass 'page' + args to the step function
122
+ await stepDef.fn(page, ...args);
105
123
  }
106
124
  catch (error) {
107
- // 3. CAPTURE EVIDENCE ON FAILURE
108
125
  console.error(`❌ Failed at step: "${stepText}"`);
109
126
  // Take a screenshot immediately
110
127
  const screenshot = await page.screenshot({
111
128
  fullPage: true,
112
129
  type: "png",
113
130
  });
114
- // Attach it to the report (HTML/JSON/Slack will pick this up)
115
131
  await testInfo.attach("failure-screenshot", {
116
132
  body: screenshot,
117
133
  contentType: "image/png",
118
134
  });
119
- // Re-throw the error so the test is marked as Failed
120
135
  throw error;
121
136
  }
122
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-cucumber-ts-steps",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",