playwright-cucumber-ts-steps 1.0.2 → 1.0.4
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 +93 -0
- package/dist/backend/actions/formTable.js +34 -0
- package/dist/backend/actions/index.js +1 -0
- package/dist/backend/api/index.js +1 -0
- package/dist/backend/api/mock.js +75 -0
- package/dist/backend/api/requests.js +66 -4
- package/dist/backend/db/index.js +6 -0
- package/dist/backend/db/state.js +24 -0
- package/dist/backend/db/steps.js +43 -0
- package/dist/core/runner.js +35 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# 🎭 Playwright Cucumber TS Steps
|
|
2
2
|
|
|
3
|
+
[](https://nodei.co/npm/playwright-cucumber-ts-steps/)
|
|
4
|
+
|
|
5
|
+
<table align="center" style="margin-bottom:30px;"><tr><td align="center" width="9999" heigth="9999" >
|
|
6
|
+
<img src="https://github.com/qaPaschalE/k6-cucumber-steps/blob/main/assets/paschal%20logo%20(2).png?raw=true" alt="paschal Logo" style="margin-top:25px;" align="center"/>
|
|
7
|
+
</td></tr></table>
|
|
8
|
+
|
|
3
9
|
[](https://www.npmjs.com/package/playwright-cucumber-ts-steps)
|
|
4
10
|
[](https://www.npmjs.com/package/playwright-cucumber-ts-steps)
|
|
5
11
|
[](https://opensource.org/licenses/MIT)
|
|
6
12
|
[](https://playwright.dev)
|
|
7
13
|
[](https://www.typescriptlang.org/)
|
|
14
|
+
[](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/stargazers)
|
|
8
15
|
|
|
9
16
|
**The "Low-Code" BDD Framework for Playwright.**
|
|
10
17
|
|
|
@@ -193,6 +200,92 @@ Feature: Admin Panel
|
|
|
193
200
|
|
|
194
201
|
```
|
|
195
202
|
|
|
203
|
+
### 4. Data Tables (Forms)
|
|
204
|
+
|
|
205
|
+
Fill out entire forms in a single step using a Data Table. You can type, click, check, or assert visibility in one go.
|
|
206
|
+
|
|
207
|
+
```gherkin
|
|
208
|
+
Scenario: Registration
|
|
209
|
+
When I fill the following "Registration" form data:
|
|
210
|
+
| #first-name | John |
|
|
211
|
+
| #last-name | Doe |
|
|
212
|
+
| #email | john@test.com |
|
|
213
|
+
| #newsletter | check |
|
|
214
|
+
| #submit-btn | click |
|
|
215
|
+
| .success | assert:visible |
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 5. API Testing (Tables & Files)
|
|
219
|
+
|
|
220
|
+
Validate your backend directly. You can send payloads via Tables or JSON Files.
|
|
221
|
+
|
|
222
|
+
#### Option A: Data Table Payload
|
|
223
|
+
|
|
224
|
+
```gherkin
|
|
225
|
+
Scenario: Create User (Table)
|
|
226
|
+
When I make a POST request to "[https://reqres.in/api/users](https://reqres.in/api/users)" with data:
|
|
227
|
+
| name | Neo |
|
|
228
|
+
| job | The Chosen |
|
|
229
|
+
Then I expect the response status to be 201
|
|
230
|
+
And I expect the response property "name" to be "Neo"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Option B: File Payload
|
|
234
|
+
|
|
235
|
+
```gherkin
|
|
236
|
+
Scenario: Create User (File)
|
|
237
|
+
# Reads from 'data/user.json' in your project root
|
|
238
|
+
When I make a POST request to "/api/users" with payload from "data/user.json"
|
|
239
|
+
Then I expect the response status to be 201
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 6. Network Mocking
|
|
243
|
+
|
|
244
|
+
Simulate backend responses to test UI behavior without relying on real APIs.
|
|
245
|
+
|
|
246
|
+
```gherkin
|
|
247
|
+
Scenario: Mocking User Profile
|
|
248
|
+
# Intercept calls to /api/user/1 and return fake data
|
|
249
|
+
Given I mock the API endpoint "*/**/api/user/1" with body '{"name": "Mocked User"}'
|
|
250
|
+
|
|
251
|
+
# When the UI calls the API, it gets our fake data
|
|
252
|
+
When I visit "/profile"
|
|
253
|
+
Then I expect "#username-display" to have text "Mocked User"
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 7. Database Testing (Adapter Pattern)
|
|
258
|
+
|
|
259
|
+
You can validate database states by injecting your own DB driver into the runner.
|
|
260
|
+
|
|
261
|
+
**1. In your `bdd.spec.ts`:**
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { runTests } from "playwright-cucumber-ts-steps";
|
|
265
|
+
import pg from "pg"; // Your driver (pg, mysql, mongo, etc)
|
|
266
|
+
|
|
267
|
+
// wrapper function
|
|
268
|
+
const queryDb = async (query: string) => {
|
|
269
|
+
const client = new pg.Client(process.env.DB_URL);
|
|
270
|
+
await client.connect();
|
|
271
|
+
const res = await client.query(query);
|
|
272
|
+
await client.end();
|
|
273
|
+
return res.rows; // Must return an array of objects
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
runTests("features/*.feature", { dbQuery: queryDb });
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**2. In your `Feature file`:**
|
|
280
|
+
|
|
281
|
+
```gherkin
|
|
282
|
+
Scenario: Create User
|
|
283
|
+
When I run the database query "INSERT INTO users (name) VALUES ('Bob')"
|
|
284
|
+
Then I expect the database to return 1 record
|
|
285
|
+
And I expect the first database record to contain:
|
|
286
|
+
| name | Bob |
|
|
287
|
+
```
|
|
288
|
+
|
|
196
289
|
---
|
|
197
290
|
|
|
198
291
|
## 📖 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
|
+
});
|
|
@@ -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
|
|
17
|
-
// Usage:
|
|
18
|
-
|
|
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:
|
|
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
|
+
});
|
package/dist/core/runner.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
114
|
+
// 4. EXECUTE WITH ERROR HANDLING
|
|
102
115
|
try {
|
|
103
|
-
//
|
|
104
|
-
|
|
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
|
}
|