playwright-cucumber-ts-steps 0.0.1
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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/helpers/utils/fakerUtils.d.ts +1 -0
- package/dist/helpers/utils/fakerUtils.js +54 -0
- package/dist/helpers/utils/index.d.ts +3 -0
- package/dist/helpers/utils/index.js +3 -0
- package/dist/helpers/utils/optionsUtils.d.ts +19 -0
- package/dist/helpers/utils/optionsUtils.js +69 -0
- package/dist/helpers/utils/resolveUtils.d.ts +4 -0
- package/dist/helpers/utils/resolveUtils.js +54 -0
- package/dist/helpers/world.d.ts +18 -0
- package/dist/helpers/world.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/helpers/utils/fakerUtils.ts +68 -0
- package/helpers/utils/index.ts +3 -0
- package/helpers/utils/optionsUtils.ts +106 -0
- package/helpers/utils/resolveUtils.ts +69 -0
- package/helpers/world.ts +91 -0
- package/index.ts +4 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chetachi (Paschal) Enyimiri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# 🎭 playwright-cucumber-ts-steps
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/playwright-cucumber-ts-steps)
|
|
4
|
+
[](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/actions)
|
|
6
|
+
[](https://www.npmjs.com/package/playwright-cucumber-ts-steps)
|
|
7
|
+
[](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/issues)
|
|
8
|
+
[](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/stargazers)
|
|
9
|
+
|
|
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
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- 🧩 Plug-and-play Cucumber step definitions
|
|
17
|
+
- 🎯 Support for UI, API, mobile, iframe, session reuse, and visual testing
|
|
18
|
+
- 🗂️ Alias and Faker support for dynamic data
|
|
19
|
+
- 📸 Optional screenshot/video capture and pixel-diff comparison
|
|
20
|
+
- 🤖 Built with modern Playwright + Cucumber ecosystem
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 📦 Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install playwright-cucumber-ts-steps
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
or
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yarn add playwright-cucumber-ts-steps
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🧠 Prerequisites
|
|
39
|
+
|
|
40
|
+
Ensure your project is already set up with:
|
|
41
|
+
|
|
42
|
+
- [`@playwright/test`](https://playwright.dev/)
|
|
43
|
+
- [`@cucumber/cucumber`](https://github.com/cucumber/cucumber-js)
|
|
44
|
+
- TypeScript
|
|
45
|
+
- Cucumber IDE plugin (Optional), but Highly recommended
|
|
46
|
+
|
|
47
|
+
If not, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install --save-dev @playwright/test @cucumber/cucumber typescript ts-node
|
|
51
|
+
npx playwright install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🛠️ Usage
|
|
57
|
+
|
|
58
|
+
1. **Import the step definitions** in your Cucumber environment:
|
|
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:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// e2e/steps/index.ts
|
|
72
|
+
import "playwright-cucumber-ts-steps";
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or import specific step categories:
|
|
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**:
|
|
85
|
+
|
|
86
|
+
```gherkin
|
|
87
|
+
Feature: Login
|
|
88
|
+
|
|
89
|
+
Scenario: User logs in
|
|
90
|
+
Given I visit "/login"
|
|
91
|
+
When I find input by name "Email"
|
|
92
|
+
And I type "user@example.com"
|
|
93
|
+
And I click button "Login"
|
|
94
|
+
Then I see visible text "Welcome"
|
|
95
|
+
Then I do not see URL contains "/login"
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🧪 Step Categories
|
|
102
|
+
|
|
103
|
+
- ✅ **Assertions**: `I see text`, `I do not see text`, `I see button`, `I see value`, etc.
|
|
104
|
+
- 🎬 **Actions**: `I click`, `I type`, `I wait`, `I switch to iframe`, etc.
|
|
105
|
+
- 📄 **Forms**: `I fill the following`, aliasing, dynamic faker values
|
|
106
|
+
- 🌐 **API**: Request mocking, assertions, response validation
|
|
107
|
+
- 📱 **Mobile support**: Enable with `@mobile` tag (iPhone 13 emulation)
|
|
108
|
+
- 👁️ **Visual testing**: Enable with `@visual` tag (pixelmatch diff)
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🧰 Customization
|
|
113
|
+
|
|
114
|
+
You can extend the base `CustomWorld` and define your own steps:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// custom-world.ts
|
|
118
|
+
import { CustomWorld as BaseWorld } from "playwright-cucumber-ts-steps";
|
|
119
|
+
|
|
120
|
+
export class CustomWorld extends BaseWorld {
|
|
121
|
+
// Add your custom context or helpers here
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🔍 Tags & Aliases
|
|
128
|
+
|
|
129
|
+
- Use aliases with `@alias` syntax:
|
|
130
|
+
|
|
131
|
+
```gherkin
|
|
132
|
+
Given I get element by selector "[type='text_selector']"
|
|
133
|
+
And I store element text as "welcomeText"
|
|
134
|
+
Then I see "@welcomeText" in the element
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- Use faker:
|
|
138
|
+
|
|
139
|
+
```gherkin
|
|
140
|
+
// Here below "Email" represents a faker variable "Email: () => faker.internet.email()", Continue button containing text with action click, best for Forms
|
|
141
|
+
|
|
142
|
+
When I fill the following "example form page" form data:
|
|
143
|
+
| Target | Value |
|
|
144
|
+
| [name='email'] | Email |
|
|
145
|
+
| Continue | Click |
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 📸 Advanced Usage
|
|
151
|
+
|
|
152
|
+
These features are **optional** and can be implemented in your own `hooks.ts`:
|
|
153
|
+
|
|
154
|
+
- 📷 **Visual regression testing** with pixelmatch
|
|
155
|
+
- 🎥 **Video recording per scenario**
|
|
156
|
+
- 🔐 **Session reuse** using `storageState`
|
|
157
|
+
|
|
158
|
+
To keep this package focused, no hooks or reporting setup is included directly. See [`e2e/support/hooks.ts`](./e2e/support/hooks.ts) in the example project for reference.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 📁 Folder Structure Suggestion
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
e2e/
|
|
166
|
+
├── features/
|
|
167
|
+
├── step_definitions/
|
|
168
|
+
│ └── index.ts # import from this package
|
|
169
|
+
├── support/
|
|
170
|
+
│ └── world.ts # extends CustomWorld
|
|
171
|
+
│ └── hooks.ts # optional
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 🧪 Example Project
|
|
177
|
+
|
|
178
|
+
Want to see it in action?
|
|
179
|
+
|
|
180
|
+
👉 [https://github.com/qaPaschalE/playwright-cucumber-ts-steps](https://github.com/qaPaschalE/playwright-cucumber-ts-steps)
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🧾 License
|
|
185
|
+
|
|
186
|
+
[ISC](LICENSE)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🙋♂️ Contributing
|
|
191
|
+
|
|
192
|
+
Contributions, improvements, and suggestions are welcome! Feel free to open an issue or pull request.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 💬 Questions?
|
|
197
|
+
|
|
198
|
+
Open an issue on [GitHub Issues](https://github.com/qaPaschalE/playwright-cucumber-ts-steps/issues) or reach out via discussions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function evaluateFaker(value: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
const fakerMapping = {
|
|
4
|
+
"First Name": () => faker.person.middleName(),
|
|
5
|
+
Name: () => faker.person.middleName(),
|
|
6
|
+
"Last Name": () => faker.person.middleName(),
|
|
7
|
+
Email: () => faker.internet.email(),
|
|
8
|
+
"Phone Number": () => faker.string.numeric(10),
|
|
9
|
+
Number: () => faker.string.numeric(11),
|
|
10
|
+
"Complete Number": () => faker.string.numeric(11),
|
|
11
|
+
"App Colour": () => faker.color.rgb(),
|
|
12
|
+
"App Name": () => faker.commerce.productName(),
|
|
13
|
+
"Role Name": () => faker.person.jobTitle(),
|
|
14
|
+
"Company Name": () => faker.company.name(),
|
|
15
|
+
"Full Name": () => faker.person.fullName(),
|
|
16
|
+
"Disposable Email": () => faker.internet.email({ provider: "inboxkitten.com" }),
|
|
17
|
+
"ALpha Numeric": () => faker.string.numeric(11) + "e",
|
|
18
|
+
"Lorem Word": () => faker.lorem.sentences({ min: 1, max: 3 }),
|
|
19
|
+
Word: () => faker.lorem.word({ length: { min: 5, max: 11 } }),
|
|
20
|
+
"Current Date": () => dayjs().format("YYYY-MM-DD"),
|
|
21
|
+
"Current Date2": () => new Date().toISOString().split("T")[0],
|
|
22
|
+
MonthsFromNow: (months) => {
|
|
23
|
+
const monthsToAdd = parseInt(months || "0", 10);
|
|
24
|
+
return dayjs().add(monthsToAdd, "month").format("YYYY-MM-DD");
|
|
25
|
+
},
|
|
26
|
+
MonthsAgo: (months) => {
|
|
27
|
+
const monthsToSubtract = parseInt(months || "0", 10);
|
|
28
|
+
return dayjs().subtract(monthsToSubtract, "month").format("YYYY-MM-DD");
|
|
29
|
+
},
|
|
30
|
+
WeeksFromNow: (weeks) => {
|
|
31
|
+
const weeksToAdd = parseInt(weeks || "0", 10);
|
|
32
|
+
return dayjs().add(weeksToAdd, "week").format("YYYY-MM-DD");
|
|
33
|
+
},
|
|
34
|
+
WeeksAgo: (weeks) => {
|
|
35
|
+
const weeksToSubtract = parseInt(weeks || "0", 10);
|
|
36
|
+
return dayjs().subtract(weeksToSubtract, "week").format("YYYY-MM-DD");
|
|
37
|
+
},
|
|
38
|
+
DaysFromNow: (days) => {
|
|
39
|
+
const daysToAdd = parseInt(days || "0", 10);
|
|
40
|
+
return dayjs().add(daysToAdd, "day").format("YYYY-MM-DD");
|
|
41
|
+
},
|
|
42
|
+
DaysAgo: (days) => {
|
|
43
|
+
const daysToSubtract = parseInt(days || "0", 10);
|
|
44
|
+
return dayjs().subtract(daysToSubtract, "day").format("YYYY-MM-DD");
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
export function evaluateFaker(value) {
|
|
48
|
+
const [key, param] = value.split(":");
|
|
49
|
+
const fn = fakerMapping[key];
|
|
50
|
+
if (typeof fn === "function") {
|
|
51
|
+
return fn(param);
|
|
52
|
+
}
|
|
53
|
+
return value; // fallback to raw value if not mapped
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DataTable } from "@cucumber/cucumber";
|
|
2
|
+
import type { Locator } from "@playwright/test";
|
|
3
|
+
type ClickOptions = Parameters<Locator["click"]>[0];
|
|
4
|
+
type DblClickOptions = Parameters<Locator["dblclick"]>[0];
|
|
5
|
+
type HoverOptions = Parameters<Locator["hover"]>[0];
|
|
6
|
+
type FillOptions = Parameters<Locator["fill"]>[1];
|
|
7
|
+
type TypeOptions = Parameters<Locator["type"]>[1];
|
|
8
|
+
type CheckOptions = Parameters<Locator["check"]>[0];
|
|
9
|
+
type UncheckOptions = Parameters<Locator["uncheck"]>[0];
|
|
10
|
+
type SelectOptionOptions = Parameters<Locator["selectOption"]>[1];
|
|
11
|
+
export declare function parseClickOptions(table?: DataTable): Partial<ClickOptions>;
|
|
12
|
+
export declare function parseDblClickOptions(table?: DataTable): Partial<DblClickOptions>;
|
|
13
|
+
export declare function parseHoverOptions(table?: DataTable): Partial<HoverOptions>;
|
|
14
|
+
export declare function parseTypeOptions(table?: DataTable): Partial<TypeOptions>;
|
|
15
|
+
export declare function parseFillOptions(table?: DataTable): Partial<FillOptions>;
|
|
16
|
+
export declare function parseCheckOptions(table?: DataTable): Partial<CheckOptions>;
|
|
17
|
+
export declare function parseUncheckOptions(table?: DataTable): Partial<UncheckOptions>;
|
|
18
|
+
export declare function parseSelectOptions(table?: DataTable): Partial<SelectOptionOptions>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function parseClickOptions(table) {
|
|
2
|
+
return parseGenericOptions(table);
|
|
3
|
+
}
|
|
4
|
+
export function parseDblClickOptions(table) {
|
|
5
|
+
return parseGenericOptions(table);
|
|
6
|
+
}
|
|
7
|
+
export function parseHoverOptions(table) {
|
|
8
|
+
return parseGenericOptions(table);
|
|
9
|
+
}
|
|
10
|
+
export function parseTypeOptions(table) {
|
|
11
|
+
return parseGenericOptions(table);
|
|
12
|
+
}
|
|
13
|
+
export function parseFillOptions(table) {
|
|
14
|
+
return parseGenericOptions(table);
|
|
15
|
+
}
|
|
16
|
+
export function parseCheckOptions(table) {
|
|
17
|
+
return parseGenericOptions(table);
|
|
18
|
+
}
|
|
19
|
+
export function parseUncheckOptions(table) {
|
|
20
|
+
return parseGenericOptions(table);
|
|
21
|
+
}
|
|
22
|
+
export function parseSelectOptions(table) {
|
|
23
|
+
return parseGenericOptions(table);
|
|
24
|
+
}
|
|
25
|
+
function parseGenericOptions(table) {
|
|
26
|
+
if (!table)
|
|
27
|
+
return {};
|
|
28
|
+
const options = {};
|
|
29
|
+
const rows = table.raw();
|
|
30
|
+
for (const [key, value] of rows) {
|
|
31
|
+
switch (key) {
|
|
32
|
+
case "timeout":
|
|
33
|
+
case "delay":
|
|
34
|
+
case "clickCount":
|
|
35
|
+
options[key] = Number(value);
|
|
36
|
+
break;
|
|
37
|
+
case "force":
|
|
38
|
+
case "noWaitAfter":
|
|
39
|
+
case "strict":
|
|
40
|
+
case "trial":
|
|
41
|
+
options[key] = value === "true";
|
|
42
|
+
break;
|
|
43
|
+
case "modifiers":
|
|
44
|
+
options.modifiers = value
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((v) => v.trim());
|
|
47
|
+
break;
|
|
48
|
+
case "button":
|
|
49
|
+
if (["left", "middle", "right"].includes(value)) {
|
|
50
|
+
options.button = value;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error(`Invalid button option: "${value}"`);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case "position":
|
|
57
|
+
const [x, y] = value.split(",").map((n) => Number(n.trim()));
|
|
58
|
+
if (isNaN(x) || isNaN(y)) {
|
|
59
|
+
throw new Error(`Invalid position format: "${value}"`);
|
|
60
|
+
}
|
|
61
|
+
options.position = { x, y };
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
console.warn(`[⚠️ parseGenericOptions] Unknown option "${key}"`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CustomWorld } from "../world";
|
|
2
|
+
export declare function resolveValue(input: string): string;
|
|
3
|
+
export declare function resolveLoginValue(raw: string, world: CustomWorld): string | undefined;
|
|
4
|
+
export declare function deriveSessionName(emailOrUser: string, fallback?: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Dynamic resolver
|
|
4
|
+
export function resolveValue(input) {
|
|
5
|
+
// Uppercase = environment variable (e.g. TEST_USER)
|
|
6
|
+
if (/^[A-Z0-9_]+$/.test(input)) {
|
|
7
|
+
const envVal = process.env[input];
|
|
8
|
+
if (!envVal) {
|
|
9
|
+
throw new Error(`Environment variable ${input} not found.`);
|
|
10
|
+
}
|
|
11
|
+
return envVal;
|
|
12
|
+
}
|
|
13
|
+
// Dot = JSON reference (e.g. userData.email)
|
|
14
|
+
if (input.includes(".")) {
|
|
15
|
+
const [fileName, fieldName] = input.split(".");
|
|
16
|
+
const jsonPath = path.resolve("e2e/support/helper/test-data", `${fileName}.json`);
|
|
17
|
+
if (fs.existsSync(jsonPath)) {
|
|
18
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
19
|
+
const value = json[fieldName];
|
|
20
|
+
if (value !== undefined)
|
|
21
|
+
return value;
|
|
22
|
+
throw new Error(`Field "${fieldName}" not found in ${fileName}.json`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Default to hardcoded value
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
export function resolveLoginValue(raw, world) {
|
|
29
|
+
// ✅ Alias: @aliasName
|
|
30
|
+
if (raw.startsWith("@")) {
|
|
31
|
+
return world.data[raw.slice(1)];
|
|
32
|
+
}
|
|
33
|
+
// ✅ JSON: user.json:key
|
|
34
|
+
if (raw.includes(".json:")) {
|
|
35
|
+
const [filename, key] = raw.split(".json:");
|
|
36
|
+
const filePath = path.resolve("test-data", `${filename}.json`);
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
throw new Error(`JSON fixture not found: ${filename}.json`);
|
|
39
|
+
}
|
|
40
|
+
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
41
|
+
return fileData[key];
|
|
42
|
+
}
|
|
43
|
+
// ✅ Fallback to raw value
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
// Determine session name from email or username
|
|
47
|
+
export function deriveSessionName(emailOrUser, fallback = "default") {
|
|
48
|
+
if (!emailOrUser)
|
|
49
|
+
return `${fallback}User`;
|
|
50
|
+
const base = emailOrUser.includes("@")
|
|
51
|
+
? emailOrUser.split("@")[0]
|
|
52
|
+
: emailOrUser;
|
|
53
|
+
return `${base}User`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { World, ITestCaseHookParameter } from "@cucumber/cucumber";
|
|
2
|
+
import { Browser, Page, BrowserContext, Locator, FrameLocator } from "@playwright/test";
|
|
3
|
+
export declare class CustomWorld extends World {
|
|
4
|
+
browser: Browser;
|
|
5
|
+
context: BrowserContext;
|
|
6
|
+
page: Page;
|
|
7
|
+
elements?: Locator;
|
|
8
|
+
element?: Locator;
|
|
9
|
+
frame?: FrameLocator;
|
|
10
|
+
data: Record<string, any>;
|
|
11
|
+
logs: string[];
|
|
12
|
+
testName?: string;
|
|
13
|
+
init(testInfo?: ITestCaseHookParameter): Promise<void>;
|
|
14
|
+
getScope(): Page | FrameLocator;
|
|
15
|
+
exitIframe(): void;
|
|
16
|
+
log: (message: string) => void;
|
|
17
|
+
cleanup(testInfo?: ITestCaseHookParameter): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// support/world.ts
|
|
2
|
+
import { setWorldConstructor, World, } from "@cucumber/cucumber";
|
|
3
|
+
import { devices, } from "@playwright/test";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import * as dotenv from "dotenv";
|
|
6
|
+
dotenv.config();
|
|
7
|
+
const isHeadless = process.env.HEADLESS !== "false";
|
|
8
|
+
const slowMo = process.env.SLOWMO ? Number(process.env.SLOWMO) : 0;
|
|
9
|
+
export class CustomWorld extends World {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.data = {};
|
|
13
|
+
this.logs = [];
|
|
14
|
+
this.log = (message) => {
|
|
15
|
+
this.logs.push(message);
|
|
16
|
+
console.log(`[LOG] ${message}`);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async init(testInfo) {
|
|
20
|
+
const isMobile = testInfo?.pickle.tags.some((tag) => tag.name === "@mobile");
|
|
21
|
+
const device = isMobile ? devices["Pixel 5"] : undefined;
|
|
22
|
+
this.browser = await chromium.launch({ headless: isHeadless, slowMo });
|
|
23
|
+
this.context = await this.browser.newContext({
|
|
24
|
+
...(device || {}),
|
|
25
|
+
recordVideo: { dir: "e2e/test-artifacts/videos" },
|
|
26
|
+
});
|
|
27
|
+
this.page = await this.context.newPage();
|
|
28
|
+
this.testName = testInfo?.pickle.name;
|
|
29
|
+
this.log(`🧪 Initialized context${isMobile ? " (mobile)" : ""}`);
|
|
30
|
+
}
|
|
31
|
+
getScope() {
|
|
32
|
+
return this.frame ?? this.page;
|
|
33
|
+
}
|
|
34
|
+
exitIframe() {
|
|
35
|
+
this.frame = undefined;
|
|
36
|
+
this.log("⬅️ Exited iframe, scope is now main page");
|
|
37
|
+
}
|
|
38
|
+
async cleanup(testInfo) {
|
|
39
|
+
const failed = testInfo?.result?.status === "FAILED";
|
|
40
|
+
try {
|
|
41
|
+
await this.page?.close();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
this.log(`⚠️ Error closing page: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await this.context?.close();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
this.log(`⚠️ Error closing context: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await this.browser?.close();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
this.log(`⚠️ Error closing browser: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
setWorldConstructor(CustomWorld);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
// Supports optional parameters
|
|
5
|
+
type FakerMapping = Record<
|
|
6
|
+
string,
|
|
7
|
+
((param?: string) => string) | (() => string)
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
const fakerMapping: FakerMapping = {
|
|
11
|
+
"First Name": () => faker.person.middleName(),
|
|
12
|
+
Name: () => faker.person.middleName(),
|
|
13
|
+
"Last Name": () => faker.person.middleName(),
|
|
14
|
+
Email: () => faker.internet.email(),
|
|
15
|
+
"Phone Number": () => faker.string.numeric(10),
|
|
16
|
+
Number: () => faker.string.numeric(11),
|
|
17
|
+
"Complete Number": () => faker.string.numeric(11),
|
|
18
|
+
"App Colour": () => faker.color.rgb(),
|
|
19
|
+
"App Name": () => faker.commerce.productName(),
|
|
20
|
+
"Role Name": () => faker.person.jobTitle(),
|
|
21
|
+
"Company Name": () => faker.company.name(),
|
|
22
|
+
"Full Name": () => faker.person.fullName(),
|
|
23
|
+
"Disposable Email": () =>
|
|
24
|
+
faker.internet.email({ provider: "inboxkitten.com" }),
|
|
25
|
+
"ALpha Numeric": () => faker.string.numeric(11) + "e",
|
|
26
|
+
"Lorem Word": () => faker.lorem.sentences({ min: 1, max: 3 }),
|
|
27
|
+
Word: () => faker.lorem.word({ length: { min: 5, max: 11 } }),
|
|
28
|
+
"Current Date": () => dayjs().format("YYYY-MM-DD"),
|
|
29
|
+
"Current Date2": () => new Date().toISOString().split("T")[0],
|
|
30
|
+
|
|
31
|
+
MonthsFromNow: (months?: string) => {
|
|
32
|
+
const monthsToAdd = parseInt(months || "0", 10);
|
|
33
|
+
return dayjs().add(monthsToAdd, "month").format("YYYY-MM-DD");
|
|
34
|
+
},
|
|
35
|
+
MonthsAgo: (months?: string) => {
|
|
36
|
+
const monthsToSubtract = parseInt(months || "0", 10);
|
|
37
|
+
return dayjs().subtract(monthsToSubtract, "month").format("YYYY-MM-DD");
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
WeeksFromNow: (weeks?: string) => {
|
|
41
|
+
const weeksToAdd = parseInt(weeks || "0", 10);
|
|
42
|
+
return dayjs().add(weeksToAdd, "week").format("YYYY-MM-DD");
|
|
43
|
+
},
|
|
44
|
+
WeeksAgo: (weeks?: string) => {
|
|
45
|
+
const weeksToSubtract = parseInt(weeks || "0", 10);
|
|
46
|
+
return dayjs().subtract(weeksToSubtract, "week").format("YYYY-MM-DD");
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
DaysFromNow: (days?: string) => {
|
|
50
|
+
const daysToAdd = parseInt(days || "0", 10);
|
|
51
|
+
return dayjs().add(daysToAdd, "day").format("YYYY-MM-DD");
|
|
52
|
+
},
|
|
53
|
+
DaysAgo: (days?: string) => {
|
|
54
|
+
const daysToSubtract = parseInt(days || "0", 10);
|
|
55
|
+
return dayjs().subtract(daysToSubtract, "day").format("YYYY-MM-DD");
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function evaluateFaker(value: string): string {
|
|
60
|
+
const [key, param] = value.split(":");
|
|
61
|
+
|
|
62
|
+
const fn = fakerMapping[key];
|
|
63
|
+
if (typeof fn === "function") {
|
|
64
|
+
return fn(param);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value; // fallback to raw value if not mapped
|
|
68
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//optionsUtils.ts
|
|
2
|
+
import type { DataTable } from "@cucumber/cucumber";
|
|
3
|
+
import type { Locator } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
type ClickOptions = Parameters<Locator["click"]>[0];
|
|
6
|
+
type DblClickOptions = Parameters<Locator["dblclick"]>[0];
|
|
7
|
+
type HoverOptions = Parameters<Locator["hover"]>[0];
|
|
8
|
+
type FillOptions = Parameters<Locator["fill"]>[1];
|
|
9
|
+
type TypeOptions = Parameters<Locator["type"]>[1];
|
|
10
|
+
type CheckOptions = Parameters<Locator["check"]>[0];
|
|
11
|
+
type UncheckOptions = Parameters<Locator["uncheck"]>[0];
|
|
12
|
+
type SelectOptionOptions = Parameters<Locator["selectOption"]>[1];
|
|
13
|
+
|
|
14
|
+
type KeyboardModifier = NonNullable<
|
|
15
|
+
NonNullable<ClickOptions>["modifiers"]
|
|
16
|
+
>[number];
|
|
17
|
+
|
|
18
|
+
export function parseClickOptions(table?: DataTable): Partial<ClickOptions> {
|
|
19
|
+
return parseGenericOptions(table) as Partial<ClickOptions>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseDblClickOptions(
|
|
23
|
+
table?: DataTable
|
|
24
|
+
): Partial<DblClickOptions> {
|
|
25
|
+
return parseGenericOptions(table) as Partial<DblClickOptions>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseHoverOptions(table?: DataTable): Partial<HoverOptions> {
|
|
29
|
+
return parseGenericOptions(table) as Partial<HoverOptions>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseTypeOptions(table?: DataTable): Partial<TypeOptions> {
|
|
33
|
+
return parseGenericOptions(table) as unknown as Partial<TypeOptions>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseFillOptions(table?: DataTable): Partial<FillOptions> {
|
|
37
|
+
return parseGenericOptions(table) as Partial<FillOptions>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseCheckOptions(table?: DataTable): Partial<CheckOptions> {
|
|
41
|
+
return parseGenericOptions(table) as Partial<CheckOptions>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseUncheckOptions(
|
|
45
|
+
table?: DataTable
|
|
46
|
+
): Partial<UncheckOptions> {
|
|
47
|
+
return parseGenericOptions(table) as Partial<UncheckOptions>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseSelectOptions(
|
|
51
|
+
table?: DataTable
|
|
52
|
+
): Partial<SelectOptionOptions> {
|
|
53
|
+
return parseGenericOptions(table) as Partial<SelectOptionOptions>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseGenericOptions(table?: DataTable): Record<string, any> {
|
|
57
|
+
if (!table) return {};
|
|
58
|
+
|
|
59
|
+
const options: Record<string, any> = {};
|
|
60
|
+
const rows = table.raw();
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of rows) {
|
|
63
|
+
switch (key) {
|
|
64
|
+
case "timeout":
|
|
65
|
+
case "delay":
|
|
66
|
+
case "clickCount":
|
|
67
|
+
options[key] = Number(value);
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case "force":
|
|
71
|
+
case "noWaitAfter":
|
|
72
|
+
case "strict":
|
|
73
|
+
case "trial":
|
|
74
|
+
options[key] = value === "true";
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "modifiers":
|
|
78
|
+
options.modifiers = value
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((v) => v.trim() as KeyboardModifier);
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case "button":
|
|
84
|
+
if (["left", "middle", "right"].includes(value)) {
|
|
85
|
+
options.button = value;
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`Invalid button option: "${value}"`);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case "position":
|
|
92
|
+
const [x, y] = value.split(",").map((n) => Number(n.trim()));
|
|
93
|
+
if (isNaN(x) || isNaN(y)) {
|
|
94
|
+
throw new Error(`Invalid position format: "${value}"`);
|
|
95
|
+
}
|
|
96
|
+
options.position = { x, y };
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
console.warn(`[⚠️ parseGenericOptions] Unknown option "${key}"`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return options;
|
|
106
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { CustomWorld } from "../world";
|
|
4
|
+
|
|
5
|
+
// Dynamic resolver
|
|
6
|
+
export function resolveValue(input: string): string {
|
|
7
|
+
// Uppercase = environment variable (e.g. TEST_USER)
|
|
8
|
+
if (/^[A-Z0-9_]+$/.test(input)) {
|
|
9
|
+
const envVal = process.env[input];
|
|
10
|
+
if (!envVal) {
|
|
11
|
+
throw new Error(`Environment variable ${input} not found.`);
|
|
12
|
+
}
|
|
13
|
+
return envVal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Dot = JSON reference (e.g. userData.email)
|
|
17
|
+
if (input.includes(".")) {
|
|
18
|
+
const [fileName, fieldName] = input.split(".");
|
|
19
|
+
const jsonPath = path.resolve(
|
|
20
|
+
"e2e/support/helper/test-data",
|
|
21
|
+
`${fileName}.json`
|
|
22
|
+
);
|
|
23
|
+
if (fs.existsSync(jsonPath)) {
|
|
24
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
25
|
+
const value = json[fieldName];
|
|
26
|
+
if (value !== undefined) return value;
|
|
27
|
+
throw new Error(`Field "${fieldName}" not found in ${fileName}.json`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Default to hardcoded value
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
export function resolveLoginValue(
|
|
35
|
+
raw: string,
|
|
36
|
+
world: CustomWorld
|
|
37
|
+
): string | undefined {
|
|
38
|
+
// ✅ Alias: @aliasName
|
|
39
|
+
if (raw.startsWith("@")) {
|
|
40
|
+
return world.data[raw.slice(1)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ✅ JSON: user.json:key
|
|
44
|
+
if (raw.includes(".json:")) {
|
|
45
|
+
const [filename, key] = raw.split(".json:");
|
|
46
|
+
const filePath = path.resolve("test-data", `${filename}.json`);
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
throw new Error(`JSON fixture not found: ${filename}.json`);
|
|
49
|
+
}
|
|
50
|
+
const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
51
|
+
return fileData[key];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ✅ Fallback to raw value
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
// Determine session name from email or username
|
|
58
|
+
export function deriveSessionName(
|
|
59
|
+
emailOrUser: string,
|
|
60
|
+
fallback = "default"
|
|
61
|
+
): string {
|
|
62
|
+
if (!emailOrUser) return `${fallback}User`;
|
|
63
|
+
|
|
64
|
+
const base = emailOrUser.includes("@")
|
|
65
|
+
? emailOrUser.split("@")[0]
|
|
66
|
+
: emailOrUser;
|
|
67
|
+
|
|
68
|
+
return `${base}User`;
|
|
69
|
+
}
|
package/helpers/world.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// support/world.ts
|
|
2
|
+
import {
|
|
3
|
+
setWorldConstructor,
|
|
4
|
+
World,
|
|
5
|
+
ITestCaseHookParameter,
|
|
6
|
+
} from "@cucumber/cucumber";
|
|
7
|
+
import {
|
|
8
|
+
Browser,
|
|
9
|
+
Page,
|
|
10
|
+
BrowserContext,
|
|
11
|
+
Locator,
|
|
12
|
+
FrameLocator,
|
|
13
|
+
devices,
|
|
14
|
+
} from "@playwright/test";
|
|
15
|
+
import { chromium } from "playwright";
|
|
16
|
+
import * as dotenv from "dotenv";
|
|
17
|
+
|
|
18
|
+
dotenv.config();
|
|
19
|
+
|
|
20
|
+
const isHeadless = process.env.HEADLESS !== "false";
|
|
21
|
+
const slowMo = process.env.SLOWMO ? Number(process.env.SLOWMO) : 0;
|
|
22
|
+
|
|
23
|
+
export class CustomWorld extends World {
|
|
24
|
+
browser!: Browser;
|
|
25
|
+
context!: BrowserContext;
|
|
26
|
+
page!: Page;
|
|
27
|
+
|
|
28
|
+
elements?: Locator;
|
|
29
|
+
element?: Locator;
|
|
30
|
+
frame?: FrameLocator;
|
|
31
|
+
|
|
32
|
+
data: Record<string, any> = {};
|
|
33
|
+
logs: string[] = [];
|
|
34
|
+
testName?: string;
|
|
35
|
+
|
|
36
|
+
async init(testInfo?: ITestCaseHookParameter) {
|
|
37
|
+
const isMobile = testInfo?.pickle.tags.some(
|
|
38
|
+
(tag) => tag.name === "@mobile"
|
|
39
|
+
);
|
|
40
|
+
const device = isMobile ? devices["Pixel 5"] : undefined;
|
|
41
|
+
|
|
42
|
+
this.browser = await chromium.launch({ headless: isHeadless, slowMo });
|
|
43
|
+
|
|
44
|
+
this.context = await this.browser.newContext({
|
|
45
|
+
...(device || {}),
|
|
46
|
+
recordVideo: { dir: "e2e/test-artifacts/videos" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.page = await this.context.newPage();
|
|
50
|
+
this.testName = testInfo?.pickle.name;
|
|
51
|
+
this.log(`🧪 Initialized context${isMobile ? " (mobile)" : ""}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getScope(): Page | FrameLocator {
|
|
55
|
+
return this.frame ?? this.page;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exitIframe() {
|
|
59
|
+
this.frame = undefined;
|
|
60
|
+
this.log("⬅️ Exited iframe, scope is now main page");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log = (message: string) => {
|
|
64
|
+
this.logs.push(message);
|
|
65
|
+
console.log(`[LOG] ${message}`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async cleanup(testInfo?: ITestCaseHookParameter) {
|
|
69
|
+
const failed = testInfo?.result?.status === "FAILED";
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await this.page?.close();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.log(`⚠️ Error closing page: ${(err as Error).message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await this.context?.close();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.log(`⚠️ Error closing context: ${(err as Error).message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await this.browser?.close();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.log(`⚠️ Error closing browser: ${(err as Error).message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setWorldConstructor(CustomWorld);
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-cucumber-ts-steps",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/qaPaschalE/playwright-cucumber-ts-steps.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"playwright",
|
|
17
|
+
"cucumber",
|
|
18
|
+
"typescript",
|
|
19
|
+
"e2e",
|
|
20
|
+
"steps",
|
|
21
|
+
"step-definitions",
|
|
22
|
+
"testing",
|
|
23
|
+
"automation",
|
|
24
|
+
"bdd",
|
|
25
|
+
"api",
|
|
26
|
+
"ui",
|
|
27
|
+
"web",
|
|
28
|
+
"end-to-end",
|
|
29
|
+
"behavior-driven",
|
|
30
|
+
"mobile",
|
|
31
|
+
"visual-testing"
|
|
32
|
+
],
|
|
33
|
+
"author": "qaPaschalE",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@cucumber/cucumber": "^11.3.0",
|
|
36
|
+
"@faker-js/faker": "^9.8.0",
|
|
37
|
+
"@playwright/test": "^1.53.0",
|
|
38
|
+
"@types/pngjs": "^6.0.5",
|
|
39
|
+
"dayjs": "^1.11.13",
|
|
40
|
+
"dotenv-cli": "^8.0.0",
|
|
41
|
+
"multiple-cucumber-html-reporter": "^3.9.2",
|
|
42
|
+
"parse": "^6.1.1",
|
|
43
|
+
"pixelmatch": "^7.1.0",
|
|
44
|
+
"pngjs": "^7.0.0",
|
|
45
|
+
"ts-node": "^10.9.2",
|
|
46
|
+
"typescript": "^5.8.3"
|
|
47
|
+
},
|
|
48
|
+
"license": "ISC",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/qaPaschalE/playwright-cucumber-ts-steps/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/qaPaschalE/playwright-cucumber-ts-steps#readme"
|
|
53
|
+
}
|