playwright-cucumber-ts-steps 0.1.3 → 0.1.5
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 +174 -39
- package/{dist → lib}/actions/clickSteps.js +10 -3
- package/{dist → lib}/actions/debugSteps.js +2 -5
- package/{dist → lib}/actions/elementFindSteps.js +15 -18
- package/lib/actions/fillFormSteps.js +130 -0
- package/{dist → lib}/actions/inputSteps.js +9 -76
- package/{dist → lib}/actions/interceptionSteps.js +8 -0
- package/{dist → lib}/actions/miscSteps.js +60 -12
- package/{dist → lib}/actions/storageSteps.js +26 -4
- package/{dist → lib}/assertions/buttonAndTextVisibilitySteps.js +3 -23
- package/{dist → lib}/assertions/elementSteps.js +7 -1
- package/{dist → lib}/assertions/locationSteps.js +16 -0
- package/{dist → lib}/assertions/semanticSteps.js +16 -3
- package/{dist → lib}/assertions/visualSteps.js +9 -5
- package/lib/custom_setups/loginHooks.js +113 -0
- package/lib/helpers/hooks.js +210 -0
- package/{dist → lib}/helpers/utils/index.d.ts +1 -0
- package/{dist → lib}/helpers/utils/index.js +1 -0
- package/{dist → lib}/helpers/utils/optionsUtils.d.ts +5 -0
- package/{dist → lib}/helpers/utils/optionsUtils.js +12 -3
- package/{dist → lib}/helpers/utils/resolveUtils.d.ts +2 -0
- package/{dist → lib}/helpers/utils/resolveUtils.js +13 -3
- package/lib/helpers/utils/sessionUtils.d.ts +3 -0
- package/lib/helpers/utils/sessionUtils.js +40 -0
- package/{dist → lib}/helpers/world.d.ts +13 -0
- package/{dist → lib}/helpers/world.js +17 -10
- package/lib/iframes/frames.d.ts +1 -0
- package/{dist → lib}/index.d.ts +1 -1
- package/{dist → lib}/index.js +1 -1
- package/{dist → lib}/register.js +1 -5
- package/package.json +40 -6
- package/dist/custom_setups/globalLogin.d.ts +0 -2
- package/dist/custom_setups/globalLogin.js +0 -25
- package/dist/custom_setups/loginHooks.js +0 -141
- package/dist/helpers/hooks.js +0 -184
- package/{dist → lib}/actions/clickSteps.d.ts +0 -0
- package/{dist → lib}/actions/cookieSteps.d.ts +0 -0
- package/{dist → lib}/actions/cookieSteps.js +0 -0
- package/{dist → lib}/actions/debugSteps.d.ts +0 -0
- package/{dist → lib}/actions/elementFindSteps.d.ts +0 -0
- package/{dist/actions/inputSteps.d.ts → lib/actions/fillFormSteps.d.ts} +0 -0
- package/{dist/actions/interceptionSteps.d.ts → lib/actions/inputSteps.d.ts} +0 -0
- package/{dist/actions/miscSteps.d.ts → lib/actions/interceptionSteps.d.ts} +0 -0
- package/{dist/actions/mouseSteps.d.ts → lib/actions/miscSteps.d.ts} +0 -0
- package/{dist/actions/scrollSteps.d.ts → lib/actions/mouseSteps.d.ts} +0 -0
- package/{dist → lib}/actions/mouseSteps.js +0 -0
- package/{dist/actions/storageSteps.d.ts → lib/actions/scrollSteps.d.ts} +0 -0
- package/{dist → lib}/actions/scrollSteps.js +0 -0
- package/{dist/assertions → lib/actions}/storageSteps.d.ts +0 -0
- package/{dist → lib}/assertions/buttonAndTextVisibilitySteps.d.ts +0 -0
- package/{dist → lib}/assertions/cookieSteps.d.ts +0 -0
- package/{dist → lib}/assertions/cookieSteps.js +0 -0
- package/{dist → lib}/assertions/elementSteps.d.ts +0 -0
- package/{dist → lib}/assertions/formInputSteps.d.ts +0 -0
- package/{dist → lib}/assertions/formInputSteps.js +0 -0
- package/{dist → lib}/assertions/interceptionRequestsSteps.d.ts +0 -0
- package/{dist → lib}/assertions/interceptionRequestsSteps.js +0 -0
- package/{dist → lib}/assertions/locationSteps.d.ts +0 -0
- package/{dist → lib}/assertions/roleTestIdSteps.d.ts +0 -0
- package/{dist → lib}/assertions/roleTestIdSteps.js +0 -0
- package/{dist → lib}/assertions/semanticSteps.d.ts +0 -0
- package/{dist/assertions/visualSteps.d.ts → lib/assertions/storageSteps.d.ts} +0 -0
- package/{dist → lib}/assertions/storageSteps.js +1 -1
- /package/{dist/custom_setups/loginHooks.d.ts → lib/assertions/visualSteps.d.ts} +0 -0
- /package/{dist/helpers/hooks.d.ts → lib/custom_setups/loginHooks.d.ts} +0 -0
- /package/{dist → lib}/helpers/checkPeerDeps.d.ts +0 -0
- /package/{dist → lib}/helpers/checkPeerDeps.js +0 -0
- /package/{dist → lib}/helpers/compareSnapshots.d.ts +0 -0
- /package/{dist → lib}/helpers/compareSnapshots.js +0 -0
- /package/{dist/iframes/frames.d.ts → lib/helpers/hooks.d.ts} +0 -0
- /package/{dist → lib}/helpers/utils/fakerUtils.d.ts +0 -0
- /package/{dist → lib}/helpers/utils/fakerUtils.js +0 -0
- /package/{dist → lib}/iframes/frames.js +0 -0
- /package/{dist → lib}/register.d.ts +0 -0
package/README.md
CHANGED
|
@@ -9,17 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
> A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.
|
|
11
11
|
|
|
12
|
+
Here's the updated **README** with all the latest features and corrections integrated into your `playwright-cucumber-ts-steps` package:
|
|
13
|
+
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
## ✨ Features
|
|
15
17
|
|
|
16
18
|
- 🧩 Plug-and-play Cucumber step definitions
|
|
17
|
-
- 🎯 Support for UI
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
- 🎯 Support for **UI**, **API**, **mobile**, **iframe**, **hybrid login**, and **visual testing**
|
|
20
|
+
- 🧠 Smart **session management** via storageState, `localStorage`, `sessionStorage`, and alias reuse
|
|
21
|
+
- 🗂️ **Alias**, **Faker**, `.env`, and dynamic JSON fixture support
|
|
22
|
+
- 📸 **Screenshot** on failure, 🎥 **video recording**, and 🖼️ **visual diff** with baseline comparison
|
|
23
|
+
- 📤 Supports **file upload**, **drag-and-drop**, and **multi-user session flows**
|
|
24
|
+
- 🌐 Fully supports **API requests with inline assertions**, payload from custom folders, and session injection
|
|
25
|
+
- ✅ Compatible with both `Page` and `FrameLocator` contexts (iframe-aware)
|
|
23
26
|
|
|
24
27
|
## 📦 Installation
|
|
25
28
|
|
|
@@ -55,33 +58,14 @@ npx playwright install
|
|
|
55
58
|
|
|
56
59
|
## 🛠️ Usage
|
|
57
60
|
|
|
58
|
-
1. **
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
// e2e/steps/world.ts
|
|
62
|
-
import { setWorldConstructor } from "@cucumber/cucumber";
|
|
63
|
-
import { CustomWorld } from "playwright-cucumber-ts-steps";
|
|
64
|
-
|
|
65
|
-
setWorldConstructor(CustomWorld);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
2. **Load step definitions** from the package:
|
|
61
|
+
1. **Load step definitions** from the package:
|
|
69
62
|
|
|
70
63
|
```ts
|
|
71
64
|
// e2e/steps/index.ts
|
|
72
|
-
import "playwright-cucumber-ts-steps";
|
|
65
|
+
import "playwright-cucumber-ts-steps/register";
|
|
73
66
|
```
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
import "playwright-cucumber-ts-steps/src/assertions";
|
|
79
|
-
import "playwright-cucumber-ts-steps/src/actions";
|
|
80
|
-
import "playwright-cucumber-ts-steps/src/forms";
|
|
81
|
-
import "playwright-cucumber-ts-steps/src/api";
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
3. **Use step definitions in your feature files**:
|
|
68
|
+
2. **Use step definitions in your feature files**:
|
|
85
69
|
|
|
86
70
|
```gherkin
|
|
87
71
|
Feature: Login
|
|
@@ -149,41 +133,192 @@ export class CustomWorld extends BaseWorld {
|
|
|
149
133
|
|
|
150
134
|
## 📸 Advanced Usage
|
|
151
135
|
|
|
152
|
-
These features are **optional** and can be implemented in your own `
|
|
136
|
+
These features are **optional** and can be implemented in your own `cucumber.js`:
|
|
153
137
|
|
|
154
138
|
- 📷 **Visual regression testing** with pixelmatch
|
|
155
139
|
- 🎥 **Video recording per scenario**
|
|
156
140
|
- 🔐 **Session reuse** using `storageState`
|
|
157
141
|
|
|
158
|
-
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### ✅ Additional Supported Step Features
|
|
147
|
+
|
|
148
|
+
| Feature Type | Description |
|
|
149
|
+
| ---------------- | ------------------------------------------------------------------------------------- |
|
|
150
|
+
| `fill form data` | Use `When I fill the following "Login" form data:` to perform actions like: |
|
|
151
|
+
| | - `fill`, `click`, `check`, `select`, `upload:<file>` |
|
|
152
|
+
| | - `drag:<targetSelector>` for drag-and-drop |
|
|
153
|
+
| | - `assert:visible`, `assert:text:<value>` |
|
|
154
|
+
| | - `request:<METHOD>:<URL>:<payload>.json` |
|
|
155
|
+
| | - `set:localStorage:<key>`, `set:sessionStorage:<key>` |
|
|
156
|
+
| | - `wait:<ms>`, `reload`, and use alias `@aliasName` |
|
|
157
|
+
| Session Handling | Steps like `I login with a session data "admin.json"`, `I save session as "customer"` |
|
|
158
|
+
| Session Restore | `I restore session cookies "customer" [with reload] [using localStorage]` |
|
|
159
|
+
| API Assertions | Validate JSON response with `assert:json:key=expectedValue` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### ✅ Extended Usage Examples
|
|
164
|
+
|
|
165
|
+
```gherkin
|
|
166
|
+
Scenario: Login and save session
|
|
167
|
+
When I fill the following "Login" form data:
|
|
168
|
+
| input[name='email'] | test@example.com |
|
|
169
|
+
| input[name='password'] | @userPassword |
|
|
170
|
+
| input[type='checkbox'] | check |
|
|
171
|
+
| button:has-text("Sign In") | click |
|
|
172
|
+
| .welcome | assert:visible |
|
|
173
|
+
| .role | assert:text:Admin |
|
|
174
|
+
And I save session as "admin"
|
|
175
|
+
|
|
176
|
+
Scenario: Restore user session
|
|
177
|
+
Given I restore session cookies "admin" with reload using localStorage
|
|
178
|
+
When I visit "/dashboard"
|
|
179
|
+
Then I see text "Welcome back"
|
|
180
|
+
|
|
181
|
+
Scenario: API login + inject session
|
|
182
|
+
When I fill the following "Login" form data:
|
|
183
|
+
| request:POST:/api/login:adminPayload.json | saveAs:loginData |
|
|
184
|
+
| set:localStorage:token | @loginData.token |
|
|
185
|
+
| save session as | adminViaAPI |
|
|
186
|
+
```
|
|
159
187
|
|
|
160
188
|
---
|
|
161
189
|
|
|
162
|
-
|
|
190
|
+
### ✅ Folder Structure Suggestion
|
|
163
191
|
|
|
164
192
|
```text
|
|
165
193
|
e2e/
|
|
166
|
-
├── features/
|
|
194
|
+
├── features/ # .feature files
|
|
167
195
|
├── step_definitions/
|
|
168
|
-
│ └── index.ts
|
|
196
|
+
│ └── index.ts # import from this package
|
|
169
197
|
├── support/
|
|
170
|
-
│
|
|
171
|
-
│
|
|
198
|
+
│ ├── world.ts # CustomWorld extends with iframe support
|
|
199
|
+
│ ├── hooks.ts # artifact & session manager
|
|
200
|
+
│ └── helpers/
|
|
201
|
+
│ └── resolveUtils.ts # alias/env/json resolver
|
|
202
|
+
├── test-data/ # JSON fixtures
|
|
203
|
+
├── payload/ # API request payloads
|
|
172
204
|
```
|
|
173
205
|
|
|
174
206
|
---
|
|
175
207
|
|
|
176
|
-
|
|
208
|
+
Here’s a full `cucumber.js` config file that includes **all the configurable options** integrated so far. This supports:
|
|
177
209
|
|
|
178
|
-
|
|
210
|
+
- ✅ CLI/env override for artifact directories
|
|
211
|
+
- ✅ Visual testing toggle
|
|
212
|
+
- ✅ Screenshot and video toggle
|
|
213
|
+
- ✅ Device emulation via `MOBILE_DEVICE` env or world param
|
|
214
|
+
- ✅ Multiple profiles (`default`, `mobile`, `visual`, `ci`)
|
|
215
|
+
- ✅ Parallel test execution and ts-node for TypeScript support
|
|
179
216
|
|
|
180
|
-
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### ✅ `cucumber.js`
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
import path from "path";
|
|
223
|
+
import dotenv from "dotenv";
|
|
224
|
+
|
|
225
|
+
dotenv.config();
|
|
226
|
+
|
|
227
|
+
const ARTIFACT_DIR = process.env.TEST_ARTIFACT_DIR || "test-artifacts";
|
|
228
|
+
|
|
229
|
+
const defaultWorldParams = {
|
|
230
|
+
artifactDir: ARTIFACT_DIR,
|
|
231
|
+
payloadDir: "payloads",
|
|
232
|
+
enableScreenshots: process.env.ENABLE_SCREENSHOTS !== "false",
|
|
233
|
+
enableVideos: process.env.ENABLE_VIDEOS !== "false",
|
|
234
|
+
enableVisualTest: process.env.ENABLE_VISUAL_TEST === "true",
|
|
235
|
+
device: process.env.MOBILE_DEVICE || undefined, // e.g., "Pixel 5"
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default {
|
|
239
|
+
default: {
|
|
240
|
+
require: ["ts-node/register", "src/test/steps/**/*.ts", "src/test/support/**/*.ts"],
|
|
241
|
+
format: ["progress", `html:${path.join(ARTIFACT_DIR, "report.html")}`],
|
|
242
|
+
publishQuiet: true,
|
|
243
|
+
paths: ["src/test/features/**/*.feature"],
|
|
244
|
+
parallel: 2,
|
|
245
|
+
worldParameters: defaultWorldParams,
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
mobile: {
|
|
249
|
+
require: ["ts-node/register", "src/test/steps/**/*.ts", "src/test/support/**/*.ts"],
|
|
250
|
+
format: ["progress"],
|
|
251
|
+
publishQuiet: true,
|
|
252
|
+
paths: ["src/test/features/**/*.feature"],
|
|
253
|
+
parallel: 1,
|
|
254
|
+
tags: "@mobile",
|
|
255
|
+
worldParameters: {
|
|
256
|
+
...defaultWorldParams,
|
|
257
|
+
device: process.env.MOBILE_DEVICE || "iPhone 13 Pro",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
visual: {
|
|
262
|
+
require: ["ts-node/register", "src/test/steps/**/*.ts", "src/test/support/**/*.ts"],
|
|
263
|
+
format: ["progress"],
|
|
264
|
+
publishQuiet: true,
|
|
265
|
+
paths: ["src/test/features/**/*.feature"],
|
|
266
|
+
tags: "@visual",
|
|
267
|
+
worldParameters: {
|
|
268
|
+
...defaultWorldParams,
|
|
269
|
+
enableVisualTest: true,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
ci: {
|
|
274
|
+
require: ["ts-node/register", "src/test/steps/**/*.ts", "src/test/support/**/*.ts"],
|
|
275
|
+
format: ["progress", `json:${path.join(ARTIFACT_DIR, "report.json")}`],
|
|
276
|
+
publishQuiet: true,
|
|
277
|
+
paths: ["src/test/features/**/*.feature"],
|
|
278
|
+
parallel: 4,
|
|
279
|
+
worldParameters: {
|
|
280
|
+
...defaultWorldParams,
|
|
281
|
+
enableScreenshots: true,
|
|
282
|
+
enableVideos: true,
|
|
283
|
+
enableVisualTest: false,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
```
|
|
181
288
|
|
|
182
289
|
---
|
|
183
290
|
|
|
291
|
+
### ✅ Usage Examples
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# Run default suite
|
|
295
|
+
npx cucumber-js --config cucumber.js
|
|
296
|
+
|
|
297
|
+
# Run mobile tests with device from env
|
|
298
|
+
MOBILE_DEVICE="Pixel 5" npx cucumber-js --config cucumber.js --profile mobile
|
|
299
|
+
|
|
300
|
+
# Run visual regression tests
|
|
301
|
+
ENABLE_VISUAL_TEST=true npx cucumber-js --config cucumber.js --profile visual
|
|
302
|
+
|
|
303
|
+
# Run in CI profile with JSON output
|
|
304
|
+
npx cucumber-js --config cucumber.js --profile ci
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### ✅ Summary of Supported Options
|
|
310
|
+
|
|
311
|
+
| Setting | CLI/Env Variable | `worldParameters` Key | Purpose |
|
|
312
|
+
| ------------------ | -------------------- | --------------------- | ------------------------------------------- |
|
|
313
|
+
| Screenshot toggle | `ENABLE_SCREENSHOTS` | `enableScreenshots` | Capture screenshots on failure |
|
|
314
|
+
| Video toggle | `ENABLE_VIDEOS` | `enableVideos` | Enable/disable video recording |
|
|
315
|
+
| Visual testing | `ENABLE_VISUAL_TEST` | `enableVisualTest` | Capture and compare visual snapshots |
|
|
316
|
+
| Artifact directory | `TEST_ARTIFACT_DIR` | `artifactDir` | Where to save screenshots, videos, etc. |
|
|
317
|
+
| Mobile device emu | `MOBILE_DEVICE` | `device` | Device name for Playwright mobile emulation |
|
|
318
|
+
|
|
184
319
|
## 🧾 License
|
|
185
320
|
|
|
186
|
-
[
|
|
321
|
+
[MIT](LICENSE)
|
|
187
322
|
|
|
188
323
|
---
|
|
189
324
|
|
|
@@ -11,6 +11,14 @@ const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
|
11
11
|
await this.element.click(options);
|
|
12
12
|
this.log?.("🖱️ Clicked on stored element");
|
|
13
13
|
});
|
|
14
|
+
(0, cucumber_1.When)("I click on element {string}", async function (selector, ...rest) {
|
|
15
|
+
const maybeTable = rest[0];
|
|
16
|
+
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
17
|
+
const element = this.getLocator(selector);
|
|
18
|
+
await element.click(options);
|
|
19
|
+
this.element = element;
|
|
20
|
+
this.log?.(`🖱️ Clicked on element "${selector}"`);
|
|
21
|
+
});
|
|
14
22
|
(0, cucumber_1.When)("I click on button {string}", async function (label, ...rest) {
|
|
15
23
|
const maybeTable = rest[0];
|
|
16
24
|
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseClickOptions)(maybeTable) : {};
|
|
@@ -151,8 +159,7 @@ const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
|
151
159
|
this.log?.(`✅ Clicked all ${count} stored elements.`);
|
|
152
160
|
});
|
|
153
161
|
(0, cucumber_1.When)(/^I click on selector "([^"]+)"$/, async function (selector) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
await element.click();
|
|
162
|
+
const locator = this.getLocator(selector);
|
|
163
|
+
await locator.click();
|
|
157
164
|
this.log?.(`🖱️ Clicked on selector: ${selector}`);
|
|
158
165
|
});
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
// e2e/step_definitions/common/actions/debugSteps.ts
|
|
4
4
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
|
-
(0, cucumber_1.When)("I debug", async function () {
|
|
5
|
+
(0, cucumber_1.When)("I debug with message {string}", async function (message) {
|
|
6
6
|
await this.page.pause();
|
|
7
|
-
this.log(
|
|
8
|
-
});
|
|
9
|
-
(0, cucumber_1.When)("I log {string}", async function (msg) {
|
|
10
|
-
this.log(msg);
|
|
7
|
+
this.log(`Paused test for debugging: ${message}`);
|
|
11
8
|
});
|
|
@@ -9,6 +9,15 @@ const test_1 = require("@playwright/test");
|
|
|
9
9
|
this.element = this.page.locator(selector);
|
|
10
10
|
await (0, test_1.expect)(this.element).toHaveCount(1);
|
|
11
11
|
});
|
|
12
|
+
(0, cucumber_1.When)("I find link by text {string}", async function (text) {
|
|
13
|
+
this.currentLocator = this.getScope().getByRole("link", { name: text });
|
|
14
|
+
});
|
|
15
|
+
(0, cucumber_1.When)("I find heading by text {string}", async function (text) {
|
|
16
|
+
this.currentLocator = this.getScope().getByRole("heading", { name: text });
|
|
17
|
+
});
|
|
18
|
+
(0, cucumber_1.When)("I find headings by text {string}", async function (text) {
|
|
19
|
+
this.currentLocator = this.getScope().getByRole("heading", { name: text });
|
|
20
|
+
});
|
|
12
21
|
(0, cucumber_1.When)("I find elements by selector {string}", async function (selector) {
|
|
13
22
|
this.elements = this.page.locator(selector);
|
|
14
23
|
const count = await this.elements.count();
|
|
@@ -38,6 +47,9 @@ const test_1 = require("@playwright/test");
|
|
|
38
47
|
this.element = this.page.getByLabel(label);
|
|
39
48
|
await (0, test_1.expect)(this.element).toHaveCount(1);
|
|
40
49
|
});
|
|
50
|
+
(0, cucumber_1.When)("I find elements by label text {string}", async function (label) {
|
|
51
|
+
this.currentLocator = this.getScope().getByLabel(label);
|
|
52
|
+
});
|
|
41
53
|
(0, cucumber_1.When)("I find element by alt text {string}", async function (alt) {
|
|
42
54
|
this.element = this.page.getByAltText(alt);
|
|
43
55
|
await (0, test_1.expect)(this.element).toHaveCount(1);
|
|
@@ -46,6 +58,9 @@ const test_1 = require("@playwright/test");
|
|
|
46
58
|
this.element = this.page.getByRole("textbox", { name });
|
|
47
59
|
await (0, test_1.expect)(this.element).toHaveCount(1);
|
|
48
60
|
});
|
|
61
|
+
(0, cucumber_1.When)("I find elements by name {string}", async function (name) {
|
|
62
|
+
this.currentLocator = this.getScope().locator(`[name="${name}"]`);
|
|
63
|
+
});
|
|
49
64
|
(0, cucumber_1.When)("I find buttons by text {string}", async function (buttonText) {
|
|
50
65
|
// 🧠 Resolve alias
|
|
51
66
|
if (buttonText.startsWith("@")) {
|
|
@@ -100,24 +115,6 @@ const test_1 = require("@playwright/test");
|
|
|
100
115
|
this.elements = locator;
|
|
101
116
|
this.log?.(`Stored ${count} elements with role "${role}"`);
|
|
102
117
|
});
|
|
103
|
-
// When(
|
|
104
|
-
// "I get {int}{ordinal} element",
|
|
105
|
-
// async function (this: CustomWorld, index: number) {
|
|
106
|
-
// if (!this.elements) {
|
|
107
|
-
// throw new Error(
|
|
108
|
-
// "No element collection found. Did you call 'find elements by' first?"
|
|
109
|
-
// );
|
|
110
|
-
// }
|
|
111
|
-
// const total = await this.elements.count();
|
|
112
|
-
// if (index < 1 || index > total) {
|
|
113
|
-
// throw new Error(
|
|
114
|
-
// `Index ${index} out of bounds. Found only ${total} elements.`
|
|
115
|
-
// );
|
|
116
|
-
// }
|
|
117
|
-
// this.element = this.elements.nth(index - 1);
|
|
118
|
-
// this.log?.(`Stored ${index}th element from collection`);
|
|
119
|
-
// }
|
|
120
|
-
// );
|
|
121
118
|
(0, cucumber_1.When)("I get {int}rd element", async function (index) {
|
|
122
119
|
if (!this.elements)
|
|
123
120
|
throw new Error("No element collection found");
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const cucumber_1 = require("@cucumber/cucumber");
|
|
9
|
+
const test_1 = require("@playwright/test");
|
|
10
|
+
const resolveUtils_1 = require("../helpers/utils/resolveUtils");
|
|
11
|
+
(0, cucumber_1.When)("I fill the following {string} form data:", async function (_formName, dataTable) {
|
|
12
|
+
// const scope = this.frame ?? this.page;
|
|
13
|
+
const rows = dataTable.hashes();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const target = row.Target.trim();
|
|
16
|
+
const rawValue = row.Value.trim();
|
|
17
|
+
const locator = this.getLocator(target);
|
|
18
|
+
const value = (0, resolveUtils_1.resolveLoginValue)(rawValue, this);
|
|
19
|
+
// ✅ Assertions
|
|
20
|
+
if (rawValue.startsWith("assert:")) {
|
|
21
|
+
const [, type, expected] = rawValue.split(":");
|
|
22
|
+
if (type === "visible") {
|
|
23
|
+
await (0, test_1.expect)(locator).toBeVisible();
|
|
24
|
+
}
|
|
25
|
+
else if (type === "text") {
|
|
26
|
+
await (0, test_1.expect)(locator).toHaveText(expected ?? "", {
|
|
27
|
+
useInnerText: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else if (type === "value") {
|
|
31
|
+
await (0, test_1.expect)(locator).toHaveValue(expected ?? "");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`❌ Unknown assertion: ${type}`);
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// ✅ UI interactions
|
|
39
|
+
if (rawValue === "click") {
|
|
40
|
+
await locator.click();
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (rawValue === "check") {
|
|
44
|
+
await locator.check();
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (rawValue === "uncheck") {
|
|
48
|
+
await locator.uncheck();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (rawValue === "select") {
|
|
52
|
+
await locator.selectOption({ index: 0 });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// ✅ File upload
|
|
56
|
+
if (rawValue.startsWith("upload:")) {
|
|
57
|
+
const filePath = rawValue.split("upload:")[1].trim();
|
|
58
|
+
const resolvedPath = path_1.default.resolve(filePath);
|
|
59
|
+
if (!fs_1.default.existsSync(resolvedPath))
|
|
60
|
+
throw new Error(`File not found: ${filePath}`);
|
|
61
|
+
await locator.setInputFiles(resolvedPath);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// ✅ Drag and drop
|
|
65
|
+
if (rawValue.startsWith("drag:")) {
|
|
66
|
+
const targetSelector = rawValue.split("drag:")[1].trim();
|
|
67
|
+
const targetLocator = this.getLocator(targetSelector);
|
|
68
|
+
await locator.dragTo(targetLocator);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// ✅ Local/sessionStorage
|
|
72
|
+
if (rawValue.startsWith("set:localStorage:")) {
|
|
73
|
+
const [, , key] = rawValue.split(":");
|
|
74
|
+
if (typeof key !== "string" || !key) {
|
|
75
|
+
throw new Error("Local storage key must be a non-empty string");
|
|
76
|
+
}
|
|
77
|
+
await this.page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value ?? ""]);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (rawValue.startsWith("set:sessionStorage:")) {
|
|
81
|
+
const [, , key] = rawValue.split(":");
|
|
82
|
+
if (typeof key !== "string" || key === undefined) {
|
|
83
|
+
throw new Error("Session storage key must be a string");
|
|
84
|
+
}
|
|
85
|
+
await this.page.evaluate((args) => {
|
|
86
|
+
const [k, v] = args;
|
|
87
|
+
sessionStorage.setItem(k, v);
|
|
88
|
+
}, [key, value ?? ""]);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// ✅ Wait
|
|
92
|
+
if (rawValue.startsWith("wait:")) {
|
|
93
|
+
const [, timeMs] = rawValue.split(":");
|
|
94
|
+
const waitTime = Number(timeMs);
|
|
95
|
+
if (!isNaN(waitTime)) {
|
|
96
|
+
await this.page.waitForTimeout(waitTime);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// ✅ Reload
|
|
101
|
+
if (rawValue === "reload") {
|
|
102
|
+
await this.page.reload();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// ✅ Request handling
|
|
106
|
+
if (rawValue.startsWith("request:")) {
|
|
107
|
+
const [, method, url, file] = rawValue.replace("request:", "").split(":");
|
|
108
|
+
const payloadDir = row.PayloadDir || this.parameters?.payloadDir || "payload";
|
|
109
|
+
const filePath = path_1.default.resolve(payloadDir, file);
|
|
110
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
111
|
+
throw new Error(`Payload file not found: ${filePath}`);
|
|
112
|
+
}
|
|
113
|
+
const payload = JSON.parse(fs_1.default.readFileSync(filePath, "utf-8"));
|
|
114
|
+
const response = await this.page.request[method.toLowerCase()](url, {
|
|
115
|
+
data: payload,
|
|
116
|
+
});
|
|
117
|
+
const responseBody = await response.json();
|
|
118
|
+
this.data.lastApiResponse = responseBody;
|
|
119
|
+
this.data.lastStatusCode = response.status();
|
|
120
|
+
if (row.SaveAs) {
|
|
121
|
+
this.data[row.SaveAs] = responseBody;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// ✅ Default: fill
|
|
126
|
+
if (value !== undefined) {
|
|
127
|
+
await locator.fill(String(value));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
// e2e/step_definitions/common/actions/inputSteps.ts
|
|
4
|
+
// import fs from "fs";
|
|
5
|
+
// import path from "path";
|
|
7
6
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
8
|
-
const test_1 = require("@playwright/test");
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const fs_1 = __importDefault(require("fs"));
|
|
11
7
|
const fakerUtils_1 = require("../helpers/utils/fakerUtils");
|
|
12
8
|
const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
13
9
|
(0, cucumber_1.When)("I check", async function (...rest) {
|
|
@@ -36,80 +32,17 @@ const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
|
36
32
|
throw new Error("No input selected");
|
|
37
33
|
await this.element.uncheck(options);
|
|
38
34
|
});
|
|
39
|
-
|
|
40
|
-
const maybeOptionsTable = rest[0];
|
|
41
|
-
const globalOptions = maybeOptionsTable?.rowsHash
|
|
42
|
-
? (0, optionsUtils_1.parseClickOptions)(maybeOptionsTable)
|
|
43
|
-
: {};
|
|
44
|
-
const rows = tableData.raw().slice(1); // Remove header row
|
|
45
|
-
for (const [rawTarget, rawValue] of rows) {
|
|
46
|
-
let element;
|
|
47
|
-
let resolvedValue;
|
|
48
|
-
if (rawTarget.startsWith("@")) {
|
|
49
|
-
element = this.data[rawTarget.slice(1)];
|
|
50
|
-
if (!element)
|
|
51
|
-
throw new Error(`No element found for alias: ${rawTarget}`);
|
|
52
|
-
}
|
|
53
|
-
else if (await this.page.locator(rawTarget).count()) {
|
|
54
|
-
element = this.page.locator(rawTarget);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
element = this.page.getByText(rawTarget, { exact: true }).first();
|
|
58
|
-
}
|
|
59
|
-
if (!element)
|
|
60
|
-
throw new Error(`Unable to resolve element for target: ${rawTarget}`);
|
|
61
|
-
await element.waitFor({ state: "visible", timeout: 7000 });
|
|
62
|
-
if (rawValue.startsWith("@")) {
|
|
63
|
-
resolvedValue = this.data[rawValue.slice(1)];
|
|
64
|
-
if (!resolvedValue)
|
|
65
|
-
throw new Error(`No value found for alias: ${rawValue}`);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
resolvedValue = (0, fakerUtils_1.evaluateFaker)(rawValue);
|
|
69
|
-
}
|
|
70
|
-
if (resolvedValue === "Click") {
|
|
71
|
-
await element.click(globalOptions);
|
|
72
|
-
this.log?.(`🖱️ Clicked on "${rawTarget}"`);
|
|
73
|
-
}
|
|
74
|
-
else if (rawTarget.includes('input[type="file"]')) {
|
|
75
|
-
const filePath = path_1.default.resolve("test-data", resolvedValue);
|
|
76
|
-
if (!fs_1.default.existsSync(filePath))
|
|
77
|
-
throw new Error(`File not found: ${filePath}`);
|
|
78
|
-
await element.setInputFiles(filePath);
|
|
79
|
-
this.log?.(`📁 Uploaded file to "${rawTarget}": ${resolvedValue}`);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
const keyPattern = /{(\w+)}/g;
|
|
83
|
-
const matches = [...resolvedValue.matchAll(keyPattern)];
|
|
84
|
-
const keys = matches.map((m) => m[1]);
|
|
85
|
-
const inputText = resolvedValue.replace(keyPattern, "").trim();
|
|
86
|
-
const fillOptions = maybeOptionsTable?.rowsHash
|
|
87
|
-
? (0, optionsUtils_1.parseFillOptions)(maybeOptionsTable)
|
|
88
|
-
: {};
|
|
89
|
-
if (inputText) {
|
|
90
|
-
await element.fill(inputText, fillOptions);
|
|
91
|
-
this.log?.(`⌨️ Filled "${rawTarget}" with: ${inputText}`);
|
|
92
|
-
}
|
|
93
|
-
for (const key of keys) {
|
|
94
|
-
await element.focus();
|
|
95
|
-
await this.page.waitForTimeout(200);
|
|
96
|
-
await element.press(key);
|
|
97
|
-
this.log?.(`🎹 Pressed {${key}} on "${rawTarget}"`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
await (0, test_1.expect)(element).toBeVisible();
|
|
101
|
-
}
|
|
102
|
-
});
|
|
35
|
+
// const DEFAULT_PAYLOAD_DIR = "payload";
|
|
103
36
|
const typeStep = async function (textOrAlias, ...rest) {
|
|
104
37
|
if (!this.element)
|
|
105
38
|
throw new Error("No element selected");
|
|
106
39
|
const maybeTable = rest[0];
|
|
107
40
|
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseFillOptions)(maybeTable) : {};
|
|
108
41
|
const text = textOrAlias.startsWith("@")
|
|
109
|
-
? this.data[textOrAlias.slice(1)] ??
|
|
42
|
+
? (this.data[textOrAlias.slice(1)] ??
|
|
110
43
|
(() => {
|
|
111
44
|
throw new Error(`No value found for alias "${textOrAlias}"`);
|
|
112
|
-
})()
|
|
45
|
+
})())
|
|
113
46
|
: (0, fakerUtils_1.evaluateFaker)(textOrAlias);
|
|
114
47
|
await this.element.fill("");
|
|
115
48
|
await this.element.fill(text, options);
|
|
@@ -125,10 +58,10 @@ const typeStep = async function (textOrAlias, ...rest) {
|
|
|
125
58
|
const maybeTable = rest[0];
|
|
126
59
|
const options = maybeTable?.rowsHash ? (0, optionsUtils_1.parseFillOptions)(maybeTable) : {};
|
|
127
60
|
const value = valueOrAlias.startsWith("@")
|
|
128
|
-
? this.data[valueOrAlias.slice(1)] ??
|
|
61
|
+
? (this.data[valueOrAlias.slice(1)] ??
|
|
129
62
|
(() => {
|
|
130
63
|
throw new Error(`No value found for alias "${valueOrAlias}"`);
|
|
131
|
-
})()
|
|
64
|
+
})())
|
|
132
65
|
: (0, fakerUtils_1.evaluateFaker)(valueOrAlias);
|
|
133
66
|
await this.element.fill(value, options);
|
|
134
67
|
this.data.lastValueSet = value;
|
|
@@ -140,8 +73,8 @@ const typeStep = async function (textOrAlias, ...rest) {
|
|
|
140
73
|
await this.element.fill("");
|
|
141
74
|
this.log?.("🧼 Cleared value of selected element");
|
|
142
75
|
});
|
|
143
|
-
(0, cucumber_1.When)("I submit", async function (
|
|
144
|
-
const maybeTable = rest[0];
|
|
76
|
+
(0, cucumber_1.When)("I submit", async function () {
|
|
77
|
+
// const maybeTable = rest[0];
|
|
145
78
|
const form = this.element ?? this.page.locator("form");
|
|
146
79
|
await form.evaluate((f) => f.submit());
|
|
147
80
|
this.log?.("📨 Submitted form");
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
// e2e/step_definitions/common/interceptionSteps.ts
|
|
4
4
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
|
+
let lastResponse;
|
|
5
6
|
(0, cucumber_1.When)("I intercept URL {string} and stub body:", async function (url, body) {
|
|
6
7
|
let parsedBody;
|
|
7
8
|
try {
|
|
@@ -61,3 +62,10 @@ const cucumber_1 = require("@cucumber/cucumber");
|
|
|
61
62
|
const response = await this.page.request.get(url);
|
|
62
63
|
this.data.lastResponse = response;
|
|
63
64
|
});
|
|
65
|
+
(0, cucumber_1.When)('I make a "{word}" request to {string}', async function (method, url, table) {
|
|
66
|
+
const options = table ? Object.fromEntries(table.rows()) : {};
|
|
67
|
+
if (options.body)
|
|
68
|
+
options.body = JSON.stringify(JSON.parse(options.body));
|
|
69
|
+
const res = await fetch(url, { method, ...options });
|
|
70
|
+
lastResponse = res;
|
|
71
|
+
});
|