playwright-cucumber-ts-steps 0.1.5 → 0.1.7
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/package.json +6 -6
- package/src/actions/clickSteps.ts +207 -0
- package/src/actions/cookieSteps.ts +29 -0
- package/src/actions/debugSteps.ts +7 -0
- package/src/actions/elementFindSteps.ts +256 -0
- package/src/actions/fillFormSteps.ts +213 -0
- package/src/actions/inputSteps.ts +118 -0
- package/src/actions/interceptionSteps.ts +87 -0
- package/src/actions/miscSteps.ts +414 -0
- package/src/actions/mouseSteps.ts +99 -0
- package/src/actions/scrollSteps.ts +24 -0
- package/src/actions/storageSteps.ts +83 -0
- package/src/assertions/buttonAndTextVisibilitySteps.ts +178 -0
- package/src/assertions/cookieSteps.ts +52 -0
- package/src/assertions/elementSteps.ts +103 -0
- package/src/assertions/formInputSteps.ts +110 -0
- package/src/assertions/interceptionRequestsSteps.ts +216 -0
- package/src/assertions/locationSteps.ts +99 -0
- package/src/assertions/roleTestIdSteps.ts +36 -0
- package/src/assertions/semanticSteps.ts +79 -0
- package/src/assertions/storageSteps.ts +89 -0
- package/src/assertions/visualSteps.ts +98 -0
- package/src/custom_setups/loginHooks.ts +135 -0
- package/src/helpers/checkPeerDeps.ts +19 -0
- package/src/helpers/compareSnapshots.ts +35 -0
- package/src/helpers/hooks.ts +212 -0
- package/src/helpers/utils/fakerUtils.ts +64 -0
- package/src/helpers/utils/index.ts +4 -0
- package/src/helpers/utils/optionsUtils.ts +104 -0
- package/src/helpers/utils/resolveUtils.ts +74 -0
- package/src/helpers/utils/sessionUtils.ts +36 -0
- package/src/helpers/world.ts +93 -0
- package/src/iframes/frames.ts +15 -0
- package/src/index.ts +39 -0
- package/src/register.ts +4 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { When } from "@cucumber/cucumber";
|
|
4
|
+
import { expect } from "@playwright/test";
|
|
5
|
+
import { resolveLoginValue } from "../helpers/utils/resolveUtils";
|
|
6
|
+
import type { CustomWorld } from "../helpers/world";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* This file contains the step definitions for filling out forms in Playwright.
|
|
10
|
+
* It supports various actions like filling inputs, clicking buttons, checking checkboxes,
|
|
11
|
+
* uploading files, and handling requests.
|
|
12
|
+
* Feature: User login with hybrid form and API session
|
|
13
|
+
|
|
14
|
+
Background:
|
|
15
|
+
Given I open the application homepage
|
|
16
|
+
|
|
17
|
+
Scenario: Login as Admin via UI and save session
|
|
18
|
+
When I fill the following "Login" form data:
|
|
19
|
+
| Target | Value |
|
|
20
|
+
| input[placeholder='email'] | test@email.com |
|
|
21
|
+
| input[placeholder='password'] | @adminPassword |
|
|
22
|
+
| input[type='checkbox'] | check |
|
|
23
|
+
| select[name='role'] | select |
|
|
24
|
+
| button:has-text("Sign In") | click |
|
|
25
|
+
| .dashboard-header | assert:visible |
|
|
26
|
+
| .user-role | assert:text:Admin |
|
|
27
|
+
Then I save session as "sessionAdmin.json"
|
|
28
|
+
|
|
29
|
+
Scenario: Restore session and assert dashboard
|
|
30
|
+
Given I restore session cookies "sessionAdmin.json" with reload
|
|
31
|
+
Then .user-role should contain text "Admin"
|
|
32
|
+
|
|
33
|
+
Scenario: Login via API request and inject session
|
|
34
|
+
When I fill the following "Login" form data:
|
|
35
|
+
| Target | Value |
|
|
36
|
+
| request:POST:/api/auth/login:adminLogin.json | |
|
|
37
|
+
| set:localStorage:auth_token | @lastApiResponse.token |
|
|
38
|
+
| wait | wait:1000 |
|
|
39
|
+
| reload | |
|
|
40
|
+
| .dashboard-header | assert:visible |
|
|
41
|
+
Then I save session as "apiSessionAdmin.json"
|
|
42
|
+
|
|
43
|
+
Scenario: Upload, drag, and assert form
|
|
44
|
+
When I fill the following "Profile" form data:
|
|
45
|
+
| Target | Value |
|
|
46
|
+
| input[type='file'] | upload:fixtures/profile-pic.jpg |
|
|
47
|
+
| div.upload-target | drag:.upload-preview |
|
|
48
|
+
| .upload-success | assert:visible |
|
|
49
|
+
|
|
50
|
+
Scenario: Login as reviewer and save session for approval flows
|
|
51
|
+
When I fill the following "Login" form data:
|
|
52
|
+
| Target | Value |
|
|
53
|
+
| input[placeholder='email'] | reviewer@email.com |
|
|
54
|
+
| input[placeholder='password'] | REVIEWER_PASS |
|
|
55
|
+
| button:has-text("Login") | click |
|
|
56
|
+
| .user-role | assert:text:Reviewer |
|
|
57
|
+
Then I save session as "sessionReviewer.json"
|
|
58
|
+
|
|
59
|
+
*/
|
|
60
|
+
// Explicitly type each row
|
|
61
|
+
type ActionRow = {
|
|
62
|
+
Target: string;
|
|
63
|
+
Value: string;
|
|
64
|
+
PayloadDir?: string;
|
|
65
|
+
SaveAs?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
When(
|
|
69
|
+
"I fill the following {string} form data:",
|
|
70
|
+
async function (this: CustomWorld, _formName: string, dataTable) {
|
|
71
|
+
// const scope = this.frame ?? this.page;
|
|
72
|
+
const rows = dataTable.hashes() as ActionRow[];
|
|
73
|
+
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const target = row.Target.trim();
|
|
76
|
+
const rawValue = row.Value.trim();
|
|
77
|
+
const locator = this.getLocator(target);
|
|
78
|
+
const value = resolveLoginValue(rawValue, this);
|
|
79
|
+
|
|
80
|
+
// ✅ Assertions
|
|
81
|
+
if (rawValue.startsWith("assert:")) {
|
|
82
|
+
const [, type, expected] = rawValue.split(":");
|
|
83
|
+
|
|
84
|
+
if (type === "visible") {
|
|
85
|
+
await expect(locator).toBeVisible();
|
|
86
|
+
} else if (type === "text") {
|
|
87
|
+
await expect(locator).toHaveText(expected ?? "", {
|
|
88
|
+
useInnerText: true,
|
|
89
|
+
});
|
|
90
|
+
} else if (type === "value") {
|
|
91
|
+
await expect(locator).toHaveValue(expected ?? "");
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error(`❌ Unknown assertion: ${type}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ✅ UI interactions
|
|
100
|
+
if (rawValue === "click") {
|
|
101
|
+
await locator.click();
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (rawValue === "check") {
|
|
106
|
+
await locator.check();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (rawValue === "uncheck") {
|
|
111
|
+
await locator.uncheck();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (rawValue === "select") {
|
|
116
|
+
await locator.selectOption({ index: 0 });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ✅ File upload
|
|
121
|
+
if (rawValue.startsWith("upload:")) {
|
|
122
|
+
const filePath = rawValue.split("upload:")[1].trim();
|
|
123
|
+
const resolvedPath = path.resolve(filePath);
|
|
124
|
+
if (!fs.existsSync(resolvedPath)) throw new Error(`File not found: ${filePath}`);
|
|
125
|
+
await locator.setInputFiles(resolvedPath);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ✅ Drag and drop
|
|
130
|
+
if (rawValue.startsWith("drag:")) {
|
|
131
|
+
const targetSelector = rawValue.split("drag:")[1].trim();
|
|
132
|
+
const targetLocator = this.getLocator(targetSelector);
|
|
133
|
+
await locator.dragTo(targetLocator);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ✅ Local/sessionStorage
|
|
138
|
+
if (rawValue.startsWith("set:localStorage:")) {
|
|
139
|
+
const [, , key] = rawValue.split(":");
|
|
140
|
+
if (typeof key !== "string" || !key) {
|
|
141
|
+
throw new Error("Local storage key must be a non-empty string");
|
|
142
|
+
}
|
|
143
|
+
await this.page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value ?? ""]);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (rawValue.startsWith("set:sessionStorage:")) {
|
|
148
|
+
const [, , key] = rawValue.split(":");
|
|
149
|
+
if (typeof key !== "string" || key === undefined) {
|
|
150
|
+
throw new Error("Session storage key must be a string");
|
|
151
|
+
}
|
|
152
|
+
await this.page.evaluate(
|
|
153
|
+
(args: string[]) => {
|
|
154
|
+
const [k, v] = args;
|
|
155
|
+
sessionStorage.setItem(k, v);
|
|
156
|
+
},
|
|
157
|
+
[key, value ?? ""]
|
|
158
|
+
);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ✅ Wait
|
|
163
|
+
if (rawValue.startsWith("wait:")) {
|
|
164
|
+
const [, timeMs] = rawValue.split(":");
|
|
165
|
+
const waitTime = Number(timeMs);
|
|
166
|
+
if (!isNaN(waitTime)) {
|
|
167
|
+
await this.page.waitForTimeout(waitTime);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ✅ Reload
|
|
173
|
+
if (rawValue === "reload") {
|
|
174
|
+
await this.page.reload();
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ✅ Request handling
|
|
179
|
+
if (rawValue.startsWith("request:")) {
|
|
180
|
+
const [, method, url, file] = rawValue.replace("request:", "").split(":");
|
|
181
|
+
|
|
182
|
+
const payloadDir = row.PayloadDir || this.parameters?.payloadDir || "payload";
|
|
183
|
+
const filePath = path.resolve(payloadDir, file);
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(filePath)) {
|
|
186
|
+
throw new Error(`Payload file not found: ${filePath}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payload = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
190
|
+
const response = await this.page.request[
|
|
191
|
+
method.toLowerCase() as "post" | "put" | "patch" | "get"
|
|
192
|
+
](url, {
|
|
193
|
+
data: payload,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const responseBody = await response.json();
|
|
197
|
+
this.data.lastApiResponse = responseBody;
|
|
198
|
+
this.data.lastStatusCode = response.status();
|
|
199
|
+
|
|
200
|
+
if (row.SaveAs) {
|
|
201
|
+
this.data[row.SaveAs] = responseBody;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ✅ Default: fill
|
|
208
|
+
if (value !== undefined) {
|
|
209
|
+
await locator.fill(String(value));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// e2e/step_definitions/common/actions/inputSteps.ts
|
|
2
|
+
// import fs from "fs";
|
|
3
|
+
// import path from "path";
|
|
4
|
+
import { When } from "@cucumber/cucumber";
|
|
5
|
+
import { evaluateFaker } from "../helpers/utils/fakerUtils";
|
|
6
|
+
import {
|
|
7
|
+
parseCheckOptions,
|
|
8
|
+
parseFillOptions,
|
|
9
|
+
parseSelectOptions,
|
|
10
|
+
} from "../helpers/utils/optionsUtils";
|
|
11
|
+
import { CustomWorld } from "../helpers/world";
|
|
12
|
+
|
|
13
|
+
When("I check", async function (this: CustomWorld, ...rest: any[]) {
|
|
14
|
+
const maybeTable = rest[0];
|
|
15
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
16
|
+
await this.element?.check(options);
|
|
17
|
+
this.log?.("✅ Checked stored checkbox");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
When("I uncheck", async function (this: CustomWorld, ...rest: any[]) {
|
|
21
|
+
const maybeTable = rest[0];
|
|
22
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
23
|
+
await this.element?.uncheck(options);
|
|
24
|
+
this.log?.("✅ Unchecked stored checkbox");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
When("I check input", async function (this: CustomWorld, ...rest: any[]) {
|
|
28
|
+
const maybeTable = rest[0];
|
|
29
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
30
|
+
if (!this.element) throw new Error("No input selected");
|
|
31
|
+
await this.element.check(options);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
When("I uncheck input", async function (this: CustomWorld, ...rest: any[]) {
|
|
35
|
+
const maybeTable = rest[0];
|
|
36
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
37
|
+
if (!this.element) throw new Error("No input selected");
|
|
38
|
+
await this.element.uncheck(options);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// const DEFAULT_PAYLOAD_DIR = "payload";
|
|
42
|
+
|
|
43
|
+
const typeStep = async function (this: CustomWorld, textOrAlias: string, ...rest: any[]) {
|
|
44
|
+
if (!this.element) throw new Error("No element selected");
|
|
45
|
+
|
|
46
|
+
const maybeTable = rest[0];
|
|
47
|
+
const options = maybeTable?.rowsHash ? parseFillOptions(maybeTable) : {};
|
|
48
|
+
const text = textOrAlias.startsWith("@")
|
|
49
|
+
? (this.data[textOrAlias.slice(1)] ??
|
|
50
|
+
(() => {
|
|
51
|
+
throw new Error(`No value found for alias "${textOrAlias}"`);
|
|
52
|
+
})())
|
|
53
|
+
: evaluateFaker(textOrAlias);
|
|
54
|
+
|
|
55
|
+
await this.element.fill("");
|
|
56
|
+
await this.element.fill(text, options);
|
|
57
|
+
this.data.lastTyped = text;
|
|
58
|
+
this.log?.(`⌨️ Typed "${text}" into selected element`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
When("I type {string}", typeStep);
|
|
62
|
+
When("I type stored {string}", typeStep);
|
|
63
|
+
When("I type random {string}", typeStep);
|
|
64
|
+
|
|
65
|
+
When(
|
|
66
|
+
"I set value {string}",
|
|
67
|
+
async function (this: CustomWorld, valueOrAlias: string, ...rest: any[]) {
|
|
68
|
+
if (!this.element) throw new Error("No element selected");
|
|
69
|
+
|
|
70
|
+
const maybeTable = rest[0];
|
|
71
|
+
const options = maybeTable?.rowsHash ? parseFillOptions(maybeTable) : {};
|
|
72
|
+
const value = valueOrAlias.startsWith("@")
|
|
73
|
+
? (this.data[valueOrAlias.slice(1)] ??
|
|
74
|
+
(() => {
|
|
75
|
+
throw new Error(`No value found for alias "${valueOrAlias}"`);
|
|
76
|
+
})())
|
|
77
|
+
: evaluateFaker(valueOrAlias);
|
|
78
|
+
|
|
79
|
+
await this.element.fill(value, options);
|
|
80
|
+
this.data.lastValueSet = value;
|
|
81
|
+
this.log?.(`📝 Set value to "${value}"`);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
When("I clear", async function (this: CustomWorld) {
|
|
86
|
+
if (!this.element) throw new Error("No element selected");
|
|
87
|
+
await this.element.fill("");
|
|
88
|
+
this.log?.("🧼 Cleared value of selected element");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
When("I submit", async function (this: CustomWorld) {
|
|
92
|
+
// const maybeTable = rest[0];
|
|
93
|
+
const form = this.element ?? this.page.locator("form");
|
|
94
|
+
await form.evaluate((f: HTMLFormElement) => f.submit());
|
|
95
|
+
this.log?.("📨 Submitted form");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
When(
|
|
99
|
+
"I select option {string}",
|
|
100
|
+
async function (this: CustomWorld, option: string, ...rest: any[]) {
|
|
101
|
+
if (!this.element) throw new Error("No select element stored");
|
|
102
|
+
const maybeTable = rest[0];
|
|
103
|
+
const options = maybeTable?.rowsHash ? parseSelectOptions(maybeTable) : {};
|
|
104
|
+
await this.element.selectOption({ label: option }, options);
|
|
105
|
+
this.log?.(`🔽 Selected option "${option}"`);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
When(
|
|
110
|
+
"I select file {string}",
|
|
111
|
+
async function (this: CustomWorld, filePath: string, ...rest: any[]) {
|
|
112
|
+
if (!this.element) throw new Error("No file input selected");
|
|
113
|
+
const maybeTable = rest[0];
|
|
114
|
+
const options = maybeTable?.rowsHash ? parseSelectOptions(maybeTable) : {};
|
|
115
|
+
await this.element.setInputFiles(filePath, options);
|
|
116
|
+
this.log?.(`📁 Set input file to "${filePath}"`);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// e2e/step_definitions/common/interceptionSteps.ts
|
|
2
|
+
import { When, DataTable } from "@cucumber/cucumber";
|
|
3
|
+
// import { expect } from "@playwright/test";
|
|
4
|
+
import { CustomWorld } from "../helpers/world";
|
|
5
|
+
let lastResponse: any;
|
|
6
|
+
|
|
7
|
+
When("I intercept URL {string} and stub body:", async function (url: string, body: string) {
|
|
8
|
+
let parsedBody: any;
|
|
9
|
+
try {
|
|
10
|
+
parsedBody = JSON.parse(body);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
13
|
+
throw new Error(`Failed to parse JSON body: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await this.page.route(url, (route: import("playwright").Route) => {
|
|
17
|
+
route.fulfill({
|
|
18
|
+
status: 200,
|
|
19
|
+
contentType: "application/json",
|
|
20
|
+
body: JSON.stringify(parsedBody),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.log(`Intercepted and stubbed URL "${url}" with body: ${JSON.stringify(parsedBody)}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
//Making Direct API Requests (Optional, Advanced)
|
|
28
|
+
When("I make request to {string}", async function (url: string) {
|
|
29
|
+
const response = await this.page.request.get(url);
|
|
30
|
+
const status = response.status();
|
|
31
|
+
const body = await response.text();
|
|
32
|
+
this.data.lastResponse = { status, body };
|
|
33
|
+
this.log(`Made GET request to "${url}" — Status: ${status}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
When(
|
|
37
|
+
"I make a POST request to {string} with JSON body:",
|
|
38
|
+
async function (url: string, docString: string) {
|
|
39
|
+
let payload: any;
|
|
40
|
+
try {
|
|
41
|
+
payload = JSON.parse(docString);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
44
|
+
throw new Error(`Invalid JSON: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = await this.page.request.post(url, { data: payload });
|
|
48
|
+
const status = response.status();
|
|
49
|
+
const body = await response.text();
|
|
50
|
+
this.data.lastResponse = { status, body };
|
|
51
|
+
this.log(`Made POST request to "${url}" — Status: ${status}`);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
When("I intercept URL {string}", async function (this: CustomWorld, url: string) {
|
|
56
|
+
await this.page.route(url, async (route) => {
|
|
57
|
+
await route.continue();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
When(
|
|
62
|
+
"I intercept URL {string} and stub body {string}",
|
|
63
|
+
async function (this: CustomWorld, url: string, body: string) {
|
|
64
|
+
await this.page.route(url, (route) => {
|
|
65
|
+
route.fulfill({
|
|
66
|
+
status: 200,
|
|
67
|
+
contentType: "application/json",
|
|
68
|
+
body,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
When("I make a request to {string}", async function (this: CustomWorld, url: string) {
|
|
75
|
+
const response = await this.page.request.get(url);
|
|
76
|
+
this.data.lastResponse = response;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
When(
|
|
80
|
+
'I make a "{word}" request to {string}',
|
|
81
|
+
async function (method: string, url: string, table?: DataTable) {
|
|
82
|
+
const options = table ? Object.fromEntries(table.rows()) : {};
|
|
83
|
+
if (options.body) options.body = JSON.stringify(JSON.parse(options.body));
|
|
84
|
+
const res = await fetch(url, { method, ...options });
|
|
85
|
+
lastResponse = res;
|
|
86
|
+
}
|
|
87
|
+
);
|