k6-cucumber-steps 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.command.d.ts.map +1 -1
- package/dist/commands/init.command.js +19 -7
- package/dist/commands/init.command.js.map +1 -1
- package/dist/generators/feature.parser.d.ts.map +1 -1
- package/dist/generators/feature.parser.js +1 -0
- package/dist/generators/feature.parser.js.map +1 -1
- package/dist/generators/k6-script.generator.d.ts.map +1 -1
- package/dist/generators/k6-script.generator.js +199 -95
- package/dist/generators/k6-script.generator.js.map +1 -1
- package/dist/generators/project.generator.d.ts +1 -0
- package/dist/generators/project.generator.d.ts.map +1 -1
- package/dist/generators/project.generator.js +621 -142
- package/dist/generators/project.generator.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -24,6 +24,8 @@ class ProjectGenerator {
|
|
|
24
24
|
this.generateSampleFeature(outputPath);
|
|
25
25
|
this.generateBrowserSampleFeature(outputPath);
|
|
26
26
|
this.generateAuthSampleFeature(outputPath);
|
|
27
|
+
// Generate global types for k6 + browser extensions
|
|
28
|
+
this.generateGlobalTypes(outputPath);
|
|
27
29
|
// Generate sample step definitions
|
|
28
30
|
this.generateSampleSteps(outputPath, config);
|
|
29
31
|
// Generate gitignore
|
|
@@ -38,6 +40,18 @@ class ProjectGenerator {
|
|
|
38
40
|
fs_1.default.mkdirSync(dirPath, { recursive: true });
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
generateGlobalTypes(outputPath) {
|
|
44
|
+
const content = `// Auto-generated global types for k6-cucumber-steps
|
|
45
|
+
declare global {
|
|
46
|
+
var savedTokens: Record<string, any>;
|
|
47
|
+
var lastResponse: any;
|
|
48
|
+
var exportedTokens: Record<string, any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {};
|
|
52
|
+
`;
|
|
53
|
+
fs_1.default.writeFileSync(path_1.default.join(outputPath, "global.types.d.ts"), content);
|
|
54
|
+
}
|
|
41
55
|
generatePackageJson(outputPath, config) {
|
|
42
56
|
const packageJson = {
|
|
43
57
|
name: "k6-cucumber-test-project",
|
|
@@ -46,6 +60,7 @@ class ProjectGenerator {
|
|
|
46
60
|
main: config.language === "ts" ? "src/test.ts" : "test.js",
|
|
47
61
|
scripts: {
|
|
48
62
|
test: `k6 run generated/${config.language === "ts" ? "test.generated.ts" : "test.generated.js"}`,
|
|
63
|
+
testBrowserHeaded: `K6_BROWSER_HEADLESS=false K6_BROWSER_ENABLED=true k6 run generated/${config.language === "ts" ? "test.generated.ts" : "test.generated.js"}`,
|
|
49
64
|
dev: `k6 run --out json=results.json generated/${config.language === "ts" ? "test.generated.ts" : "test.generated.js"}`,
|
|
50
65
|
},
|
|
51
66
|
devDependencies: {
|
|
@@ -83,8 +98,8 @@ npm test
|
|
|
83
98
|
\`\`\`
|
|
84
99
|
|
|
85
100
|
## Project Structure
|
|
86
|
-
- \`
|
|
87
|
-
- \`steps/\` - Step definition implementations
|
|
101
|
+
- \`Features/\` - Gherkin feature files
|
|
102
|
+
- \`steps/\` - Step definition implementations
|
|
88
103
|
- \`generated/\` - Generated k6 scripts
|
|
89
104
|
`;
|
|
90
105
|
fs_1.default.writeFileSync(path_1.default.join(outputPath, "README.md"), readmeContent);
|
|
@@ -163,18 +178,118 @@ Feature: Comprehensive API Testing
|
|
|
163
178
|
fs_1.default.writeFileSync(path_1.default.join(outputPath, "features", "sample.feature"), sampleFeature);
|
|
164
179
|
}
|
|
165
180
|
generateBrowserSampleFeature(outputPath) {
|
|
166
|
-
const content = `@
|
|
167
|
-
Feature:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
const content = `@iterations:1
|
|
182
|
+
Feature: Comprehensive UI Automation on DemoQA
|
|
183
|
+
|
|
184
|
+
Background:
|
|
185
|
+
Given the base URL is "https://demoqa.com"
|
|
186
|
+
|
|
187
|
+
@browser
|
|
188
|
+
Scenario: Fill and submit the practice form successfully
|
|
189
|
+
When I navigate to the "/automation-practice-form" page
|
|
190
|
+
And I fill the field "#firstName" with "Paschal"
|
|
191
|
+
And I fill the field "#lastName" with "Enyimiri"
|
|
192
|
+
And I fill the field "#userEmail" with "paschal.cheps@example.com"
|
|
193
|
+
And I click on exact text "Male"
|
|
194
|
+
And I fill the field "#userNumber" with "0801234567"
|
|
195
|
+
And I wait "1" seconds
|
|
196
|
+
And I click on exact text "Music"
|
|
197
|
+
And I click on the element "#state"
|
|
198
|
+
And I click on exact text "NCR"
|
|
199
|
+
And I fill the field "#currentAddress" with "this is a load test on the ui"
|
|
200
|
+
And I wait "1" seconds
|
|
201
|
+
And I click on the element "#city"
|
|
202
|
+
And I click on exact text "Delhi"
|
|
203
|
+
And I click on the element "#submit"
|
|
204
|
+
Then I should see the text "Thanks for submitting the form"
|
|
205
|
+
|
|
206
|
+
Scenario: Interact using multiple locator strategies
|
|
207
|
+
When I navigate to the "/automation-practice-form" page
|
|
208
|
+
# By ID
|
|
209
|
+
And I find element by Id "firstName"
|
|
210
|
+
# By placeholder
|
|
211
|
+
And I find input element by placeholder text "First Name"
|
|
212
|
+
# By label/name (accessible)
|
|
213
|
+
And I find element by name "First Name"
|
|
214
|
+
# By role + name
|
|
215
|
+
And I find element by role "textbox" "First Name"
|
|
216
|
+
# By button text
|
|
217
|
+
And I find button by text "Submit"
|
|
218
|
+
# By value attribute
|
|
219
|
+
And I find element by value "Submit"
|
|
220
|
+
# Assertion
|
|
221
|
+
And I should see the text "Student Registration Form"
|
|
222
|
+
|
|
223
|
+
Scenario: Wait and validate dynamic content reliably
|
|
224
|
+
When I navigate to the "/automation-practice-form" page
|
|
225
|
+
And I wait "1" seconds
|
|
226
|
+
And I should see the element "#firstName"
|
|
227
|
+
And I should see the text "Practice Form"
|
|
228
|
+
And I should not see the text "Dropped!"
|
|
229
|
+
|
|
230
|
+
Scenario: Validate page metadata after navigation
|
|
231
|
+
When I navigate to the "/automation-practice-form" page
|
|
232
|
+
And the current URL should contain "automation-practice-form"
|
|
233
|
+
And the page title should be "DEMOQA"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
Scenario: Find and interact using value / role
|
|
237
|
+
Given the base URL is "https://demoqa.com"
|
|
238
|
+
When I navigate to the "/automation-practice-form" page
|
|
239
|
+
|
|
240
|
+
# Find by value attribute
|
|
241
|
+
And I find element by value "Submit"
|
|
242
|
+
And I click
|
|
243
|
+
|
|
244
|
+
# Find by role (button with text)
|
|
245
|
+
And I find element by role "button" "Submit"
|
|
246
|
+
And I click
|
|
247
|
+
|
|
248
|
+
# Find by role (heading)
|
|
249
|
+
And I find element by role "heading" "Practice Form"
|
|
250
|
+
And I should see the text "Practice Form"
|
|
251
|
+
|
|
252
|
+
# Find by role (textbox)
|
|
253
|
+
And I find element by role "textbox" "First Name"
|
|
254
|
+
And I fill "Paschal"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
Scenario: Wait and find elements
|
|
258
|
+
Given the base URL is "https://demoqa.com"
|
|
259
|
+
When I navigate to the "/automation-practice-form" page
|
|
260
|
+
And I wait "2" seconds
|
|
261
|
+
And I find input element by placeholder text "First Name"
|
|
262
|
+
And I find button by text "Submit"
|
|
263
|
+
And I find element by Id "firstName"
|
|
264
|
+
And I find elements by text "Name"
|
|
265
|
+
|
|
266
|
+
Scenario: Interact with repeated elements
|
|
267
|
+
When I navigate to the "/elements" page
|
|
268
|
+
And I fill the 1st "#input-field" with "First"
|
|
269
|
+
And I fill the 2nd "#input-field" with "Second"
|
|
270
|
+
And I click the 2nd "button[type='submit']"
|
|
271
|
+
Then I should see the 1st ".success-message"
|
|
272
|
+
# For single element (default = 1st)
|
|
273
|
+
And I click on the element "button"
|
|
274
|
+
|
|
275
|
+
# For nth element
|
|
276
|
+
And I click on the element "button" "2"
|
|
277
|
+
And I fill the field "#email" with "test@example.com" "1"
|
|
278
|
+
|
|
279
|
+
Scenario: Handle alert confirmation
|
|
280
|
+
When I navigate to the "/alerts" page
|
|
281
|
+
And I click on the element "#alertButton"
|
|
282
|
+
# Add step to accept alert if needed
|
|
283
|
+
Then I should see the text "You clicked a button"
|
|
284
|
+
|
|
285
|
+
Scenario: Drag and drop
|
|
286
|
+
When I navigate to the "/droppable" page
|
|
287
|
+
And I find element by Id "draggable"
|
|
288
|
+
And I drag to "#droppable"
|
|
289
|
+
Then I should see the text "Dropped!"
|
|
174
290
|
`;
|
|
175
291
|
fs_1.default.writeFileSync(path_1.default.join(outputPath, "features", "browserSample.feature"), content);
|
|
176
292
|
}
|
|
177
|
-
// src/generators/project.generator.ts
|
|
178
293
|
generateSampleSteps(outputPath, config) {
|
|
179
294
|
const isTS = config.language === "ts";
|
|
180
295
|
const stepExtension = isTS ? ".ts" : ".js";
|
|
@@ -191,19 +306,11 @@ let defaultHeaders${headerType} = {
|
|
|
191
306
|
'Content-Type': 'application/json'
|
|
192
307
|
};
|
|
193
308
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*/
|
|
309
|
+
/* ===== HTTP / API STEPS ===== */
|
|
310
|
+
|
|
197
311
|
export function theBaseUrlIs(url${stringType}) {
|
|
198
|
-
baseUrl = url;
|
|
312
|
+
baseUrl = url.trim();
|
|
199
313
|
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* When I authenticate with the following url and request body as {string} (as {string})
|
|
203
|
-
* This handles both:
|
|
204
|
-
* 1. ...as "user":
|
|
205
|
-
* 2. ...as "user" as "form":
|
|
206
|
-
*/
|
|
207
314
|
export function iAuthenticateWithTheFollowingUrlAndRequestBodyAs(
|
|
208
315
|
context${stringType},
|
|
209
316
|
formatOrTable${anyType},
|
|
@@ -212,7 +319,6 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAs(
|
|
|
212
319
|
let format = 'json';
|
|
213
320
|
let dataTable;
|
|
214
321
|
|
|
215
|
-
// Argument shifting logic
|
|
216
322
|
if (maybeTable === undefined) {
|
|
217
323
|
format = 'json';
|
|
218
324
|
dataTable = formatOrTable;
|
|
@@ -230,16 +336,9 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAs(
|
|
|
230
336
|
let body;
|
|
231
337
|
let params = { headers: {} };
|
|
232
338
|
|
|
233
|
-
/** * FORM FORMAT LOGIC
|
|
234
|
-
* k6 behavior: If the body is an object and Content-Type is x-www-form-urlencoded,
|
|
235
|
-
* k6 serializes the object into a query string (key=value&key2=value2).
|
|
236
|
-
*/
|
|
237
339
|
if (format === 'form') {
|
|
238
340
|
params.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
239
|
-
|
|
240
|
-
* We ensure payload is clean of any non-serializable properties.
|
|
241
|
-
*/
|
|
242
|
-
body = Object.assign({}, payload);
|
|
341
|
+
body = Object.assign({}, payload);
|
|
243
342
|
} else {
|
|
244
343
|
params.headers['Content-Type'] = 'application/json';
|
|
245
344
|
body = JSON.stringify(payload);
|
|
@@ -247,32 +346,25 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAs(
|
|
|
247
346
|
|
|
248
347
|
const response = http.post(url, body, params);
|
|
249
348
|
|
|
250
|
-
|
|
251
349
|
const success = check(response, {
|
|
252
350
|
[\`Auth successful (\${format})\`]: (r) => r.status === 200 || r.status === 201
|
|
253
351
|
});
|
|
254
352
|
|
|
255
353
|
if (success) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
354
|
+
try {
|
|
355
|
+
const parsed = response.json();
|
|
356
|
+
globalThis.lastResponse = parsed;
|
|
357
|
+
console.log(\`✅ \${context} Response Captured. Keys: \${Object.keys(parsed).join(', ')}\`);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
console.error(\`❌ Failed to parse JSON response for \${context}: \${response.body}\`);
|
|
360
|
+
globalThis.lastResponse = null;
|
|
263
361
|
}
|
|
264
|
-
|
|
265
362
|
} else {
|
|
266
363
|
console.error(\`❌ Auth failed for \${context}. Status: \${response.status}\`);
|
|
267
364
|
globalThis.lastResponse = null;
|
|
268
365
|
}
|
|
269
366
|
}
|
|
270
|
-
|
|
271
|
-
* When I authenticate with the following url and request body as {string} (as {string})
|
|
272
|
-
* This handles both:
|
|
273
|
-
* 1. ...as "user":
|
|
274
|
-
* 2. ...as "user" as "form":
|
|
275
|
-
*/
|
|
367
|
+
|
|
276
368
|
export function iAuthenticateWithTheFollowingUrlAndRequestBodyAsAs(
|
|
277
369
|
context${stringType},
|
|
278
370
|
formatOrTable${anyType},
|
|
@@ -281,7 +373,6 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAsAs(
|
|
|
281
373
|
let format = 'json';
|
|
282
374
|
let dataTable;
|
|
283
375
|
|
|
284
|
-
// Argument shifting logic
|
|
285
376
|
if (maybeTable === undefined) {
|
|
286
377
|
format = 'json';
|
|
287
378
|
dataTable = formatOrTable;
|
|
@@ -299,16 +390,9 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAsAs(
|
|
|
299
390
|
let body;
|
|
300
391
|
let params = { headers: {} };
|
|
301
392
|
|
|
302
|
-
/** * FORM FORMAT LOGIC
|
|
303
|
-
* k6 behavior: If the body is an object and Content-Type is x-www-form-urlencoded,
|
|
304
|
-
* k6 serializes the object into a query string (key=value&key2=value2).
|
|
305
|
-
*/
|
|
306
393
|
if (format === 'form') {
|
|
307
394
|
params.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
308
|
-
|
|
309
|
-
* We ensure payload is clean of any non-serializable properties.
|
|
310
|
-
*/
|
|
311
|
-
body = Object.assign({}, payload);
|
|
395
|
+
body = Object.assign({}, payload);
|
|
312
396
|
} else {
|
|
313
397
|
params.headers['Content-Type'] = 'application/json';
|
|
314
398
|
body = JSON.stringify(payload);
|
|
@@ -316,148 +400,543 @@ export function iAuthenticateWithTheFollowingUrlAndRequestBodyAsAs(
|
|
|
316
400
|
|
|
317
401
|
const response = http.post(url, body, params);
|
|
318
402
|
|
|
319
|
-
|
|
320
403
|
const success = check(response, {
|
|
321
404
|
[\`Auth successful (\${format})\`]: (r) => r.status === 200 || r.status === 201
|
|
322
405
|
});
|
|
323
406
|
|
|
324
407
|
if (success) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
408
|
+
try {
|
|
409
|
+
const parsed = response.json();
|
|
410
|
+
globalThis.lastResponse = parsed;
|
|
411
|
+
console.log(\`✅ \${context} Response Captured. Keys: \${Object.keys(parsed).join(', ')}\`);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
console.error(\`❌ Failed to parse JSON response for \${context}: \${response.body}\`);
|
|
414
|
+
globalThis.lastResponse = null;
|
|
332
415
|
}
|
|
333
|
-
|
|
334
416
|
} else {
|
|
335
417
|
console.error(\`❌ Auth failed for \${context}. Status: \${response.status}\`);
|
|
336
418
|
globalThis.lastResponse = null;
|
|
337
419
|
}
|
|
338
420
|
}
|
|
421
|
+
|
|
339
422
|
/**
|
|
340
|
-
* And I store "
|
|
423
|
+
* And I store "response.path" in "data/file.json"
|
|
424
|
+
* Example: And I store "access_token" in "data/user.json"
|
|
341
425
|
*/
|
|
342
|
-
export function iStoreIn(jsonPath
|
|
426
|
+
export function iStoreIn(jsonPath, fileName) {
|
|
343
427
|
const responseData = globalThis.lastResponse;
|
|
344
428
|
|
|
345
|
-
if (!responseData)
|
|
346
|
-
|
|
347
|
-
|
|
429
|
+
if (!responseData) {
|
|
430
|
+
console.error('❌ No response data to store. Did an HTTP request run?');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
348
433
|
|
|
349
|
-
//
|
|
350
|
-
|
|
434
|
+
// Navigate JSON path (e.g., "user.token" → responseData.user.token)
|
|
435
|
+
const value = jsonPath.split('.').reduce((acc, key) => {
|
|
436
|
+
return acc && acc[key] !== undefined ? acc[key] : undefined;
|
|
437
|
+
}, responseData);
|
|
351
438
|
|
|
352
|
-
|
|
439
|
+
if (value === undefined) {
|
|
440
|
+
console.error(\`❌ Path "\${jsonPath}" not found in response. Keys:\`, Object.keys(responseData));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
353
443
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
444
|
+
// Stage for writing in handleSummary
|
|
445
|
+
globalThis.savedTokens = globalThis.savedTokens || {};
|
|
446
|
+
globalThis.savedTokens[fileName] = value;
|
|
447
|
+
|
|
448
|
+
// Also store alias (e.g., 'user' from 'data/user.json')
|
|
449
|
+
// Remove path and .json extension
|
|
450
|
+
const alias = fileName.split(/[\\/]/).pop()?.replace(/\\.json$/, '') || fileName;
|
|
451
|
+
globalThis.savedTokens[alias] = value;
|
|
452
|
+
|
|
453
|
+
console.log(\`✅ Staged token for "\${fileName}": \${typeof value === 'string' ? '***' : JSON.stringify(value)}\`);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Background: I am authenticated as a "user"
|
|
457
|
+
* Applies stored token to default headers
|
|
458
|
+
*/
|
|
459
|
+
export function iAmAuthenticatedAsA(userType) {
|
|
460
|
+
const token = globalThis.savedTokens?.[\`data/\${userType}.json\`] ||
|
|
461
|
+
globalThis.savedTokens?.[userType];
|
|
462
|
+
|
|
463
|
+
if (token) {
|
|
464
|
+
defaultHeaders['Authorization'] = \`Bearer $\{token}\`;
|
|
465
|
+
console.log(\`🔑 Using token for $\{userType}\`);
|
|
466
|
+
} else {
|
|
467
|
+
console.warn(\`⚠️ No token found for $\{userType}\`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/* ===== BROWSER STEPS ===== */
|
|
471
|
+
|
|
472
|
+
export async function iNavigateToThePage(page, url${stringType}) {
|
|
473
|
+
let effectiveBase = baseUrl;
|
|
474
|
+
if (typeof effectiveBase !== 'string' || effectiveBase.trim() === '') {
|
|
475
|
+
console.warn('Invalid baseUrl detected:', baseUrl, '— using fallback');
|
|
476
|
+
effectiveBase = 'https://test.k6.io';
|
|
477
|
+
}
|
|
478
|
+
const fullUrl = url.startsWith('http') ? url : \`\${effectiveBase}\${url.startsWith('/') ? '' : '/'}\${url}\`;
|
|
479
|
+
console.log(\`Navigating to: \${fullUrl} (base: \${effectiveBase})\`);
|
|
480
|
+
await page.goto(fullUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export async function iClickTheButton(page, selector${stringType}) {
|
|
484
|
+
await page.locator(selector).click();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function iShouldSeeTheText(page${anyType}, expectedText${stringType}) {
|
|
488
|
+
// Reuse your existing logic or copy from iSeeTheTextOnThePage
|
|
489
|
+
let locator = page.getByRole('heading', { name: expectedText, exact: false });
|
|
490
|
+
if ((await locator.count()) === 0) {
|
|
491
|
+
locator = page.getByText(expectedText, { exact: false }).first();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
await locator.waitFor({ state: 'visible', timeout: 30000 });
|
|
496
|
+
const count = await locator.count();
|
|
497
|
+
console.log(\`Found \${count} visible elements matching "\${expectedText}"\`);
|
|
498
|
+
check(count >= 1, {
|
|
499
|
+
[\`Text/heading containing "\${expectedText}" is visible\`]: true
|
|
500
|
+
});
|
|
501
|
+
} catch (e) {
|
|
502
|
+
console.error(\`Text wait failed for "\${expectedText}":\`, e.message || e);
|
|
503
|
+
check(false, { [\`Text/heading containing "\${expectedText}" is visible\`]: false });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* === Additional Browser Steps === */
|
|
508
|
+
|
|
509
|
+
export async function iClickOnTheElement(page, selector) {
|
|
510
|
+
const locator = page.locator(selector);
|
|
511
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
512
|
+
await locator.click();
|
|
513
|
+
console.log(\`✅ Clicked element: \${selector}\`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function iFillTheFieldWith(page, selector, value) {
|
|
517
|
+
const locator = page.locator(selector);
|
|
518
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
519
|
+
await locator.fill(value);
|
|
520
|
+
console.log(\`✅ Filled field \${selector} with "\${value}"\`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function thePageTitleShouldBe(page, expectedTitle${stringType}) {
|
|
524
|
+
await page.waitForLoadState('networkidle');
|
|
525
|
+
const title = await page.title();
|
|
526
|
+
check(title, {
|
|
527
|
+
[\`Page title is "\${expectedTitle}"\`]: (t) => t === expectedTitle
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export async function theCurrentUrlShouldContain(page, expectedFragment${stringType}) {
|
|
532
|
+
const url = page.url();
|
|
533
|
+
check(url, {
|
|
534
|
+
[\`URL contains "\${expectedFragment}"\`]: (u) => u.includes(expectedFragment)
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export async function iShouldSeeTheElement(page, selector) {
|
|
539
|
+
const locator = page.locator(selector);
|
|
540
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
541
|
+
const isVisible = await locator.isVisible();
|
|
542
|
+
if (!isVisible) throw new Error(\`Element not visible: \${selector} \`);
|
|
543
|
+
console.log(\`✅ Verified visibility of element: \${selector}\`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function iShouldNotSeeTheText(page, text${stringType}) {
|
|
547
|
+
const locator = page.getByText(text, { exact: false });
|
|
548
|
+
const isHidden = (await locator.count()) === 0 || !(await locator.isVisible());
|
|
549
|
+
check(isHidden, {
|
|
550
|
+
[\`Text "\${text}" is not visible\`]: (hidden) => hidden === true
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function iSelectFromTheDropdown(page, option${stringType}, selector${stringType}) {
|
|
555
|
+
const locator = page.locator(selector);
|
|
556
|
+
await locator.selectOption(option);
|
|
557
|
+
console.log(\`Selected "\${option}" from dropdown "\${selector}"\`);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* When I drag the element "..." to "..."
|
|
561
|
+
* Example: I drag the element "#draggable" to "#droppable"
|
|
562
|
+
*/
|
|
563
|
+
export async function iDragTheElementTo(page${anyType}, sourceselector${stringType}, targetselector${stringType}) {
|
|
564
|
+
try {
|
|
565
|
+
const source = page.locator(sourceSelector);
|
|
566
|
+
const target = page.locator(targetSelector);
|
|
567
|
+
|
|
568
|
+
// Wait for both elements to be visible and stable
|
|
569
|
+
await source.waitFor({ state: 'visible', timeout: 15000 });
|
|
570
|
+
await target.waitFor({ state: 'visible', timeout: 15000 });
|
|
571
|
+
|
|
572
|
+
// Perform the drag-and-drop
|
|
573
|
+
await source.dragTo(target, {
|
|
574
|
+
// Optional: force the action if element is covered / small
|
|
575
|
+
force: true,
|
|
576
|
+
// Optional: timeout for the whole operation
|
|
577
|
+
timeout: 30000,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
console.log(\`Successfully dragged "\${sourceSelector}" to "\${targetSelector}"\`);
|
|
581
|
+
|
|
582
|
+
// Optional: small assertion that drop area changed (if it has visual feedback)
|
|
583
|
+
// await expect(target).toHaveText('Dropped!'); // if you have expect imported
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error(\`Drag failed from "\${sourceSelector}" to "\${targetSelector}":\`, error.message || error);
|
|
586
|
+
throw error; // re-throw to fail the iteration
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* And I drag to "#droppable"
|
|
591
|
+
* This step assumes the previous step returned a source locator
|
|
592
|
+
* Example usage:
|
|
593
|
+
* When I get element by selector "#draggable"
|
|
594
|
+
* And I drag to "#droppable"
|
|
595
|
+
*/
|
|
596
|
+
${isTS
|
|
597
|
+
? `export async function iDragTo(page${anyType}, targetselector${stringType}, sourceLocator?${anyType}) {`
|
|
598
|
+
: `export async function iDragTo(page, targetSelector, sourceLocator) {`}
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
// If no sourceLocator was passed (previous step didn't return it), fail early
|
|
602
|
+
if (!sourceLocator) {
|
|
603
|
+
throw new Error("No source locator provided. Did you use 'I get element by selector' before this step?");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const target = page.locator(targetSelector);
|
|
607
|
+
await target.waitFor({ state: 'visible', timeout: 15000 });
|
|
608
|
+
|
|
609
|
+
// Perform drag-and-drop from source → target
|
|
610
|
+
await sourceLocator.dragTo(target, {
|
|
611
|
+
force: true, // helpful if element is partially covered
|
|
612
|
+
timeout: 30000,
|
|
613
|
+
});
|
|
364
614
|
|
|
365
|
-
console.log(
|
|
366
|
-
|
|
615
|
+
console.log(\`Dragged source element to target: \${targetSelector}\`);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error(\`Drag to "\${targetSelector}" failed:\`, error.message || error);
|
|
618
|
+
throw error; // fail the iteration
|
|
367
619
|
}
|
|
368
620
|
}
|
|
621
|
+
export async function iWaitForTheElementToBeVisible(page, selector${stringType}) {
|
|
622
|
+
const locator = page.locator(selector);
|
|
623
|
+
await locator.waitFor({ state: 'visible', timeout: 30000 });
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* And I get element by selector "..."
|
|
627
|
+
* (just locates it, waits for visibility, but doesn't perform action)
|
|
628
|
+
*/
|
|
629
|
+
export async function iGetElementBySelector(page${anyType}, selector${stringType}) {
|
|
630
|
+
const locator = page.locator(selector);
|
|
631
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
632
|
+
console.log(\`Found element by selector: \${ selector }\`);
|
|
633
|
+
return locator; // useful if you want to chain later
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* And I find element by value "Male"
|
|
638
|
+
* (uses getByLabel – good for form labels)
|
|
639
|
+
*/
|
|
640
|
+
export async function iFindElementByLabel(page${anyType}, labelText${stringType}) {
|
|
641
|
+
const locator = page.getByLabel(labelText, { exact: false });
|
|
642
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
643
|
+
console.log(\`Found element by label: "\${labelText}"\`);
|
|
644
|
+
return locator;
|
|
645
|
+
}
|
|
646
|
+
|
|
369
647
|
/**
|
|
370
|
-
*
|
|
648
|
+
* And I find element by textarea "..."
|
|
649
|
+
* (uses getByPlaceholder or getByRole for textarea)
|
|
371
650
|
*/
|
|
372
|
-
export function
|
|
373
|
-
|
|
374
|
-
|
|
651
|
+
export async function iFindElementByTextarea(page${anyType}, placeholderOrLabel${stringType}) {
|
|
652
|
+
let locator;
|
|
653
|
+
// Try by placeholder first
|
|
654
|
+
locator = page.getByPlaceholder(placeholderOrLabel, { exact: false });
|
|
375
655
|
|
|
376
|
-
if
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
656
|
+
// Fallback to label or role if placeholder not found
|
|
657
|
+
if ((await locator.count()) === 0) {
|
|
658
|
+
locator = page.getByLabel(placeholderOrLabel, { exact: false })
|
|
659
|
+
.or(page.getByRole('textbox', { name: placeholderOrLabel }));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
663
|
+
console.log(\`Found textarea by placeholder / label: "\${placeholderOrLabel}"\`);
|
|
664
|
+
return locator;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* And I click
|
|
671
|
+
* Clicks the currently focused element (or the last interacted one).
|
|
672
|
+
* Uses real mouse click instead of keyboard Enter.
|
|
673
|
+
*/
|
|
674
|
+
export async function iClick(page${anyType}) {
|
|
675
|
+
try {
|
|
676
|
+
// Get the currently focused element (active element)
|
|
677
|
+
const focusedElement = await page.evaluateHandle(() => document.activeElement);
|
|
678
|
+
|
|
679
|
+
if (!focusedElement || focusedElement.asElement() === null) {
|
|
680
|
+
console.warn('No focused element found for click');
|
|
681
|
+
return;
|
|
388
682
|
}
|
|
683
|
+
|
|
684
|
+
// Convert Handle to Locator
|
|
685
|
+
const locator = page.locator(focusedElement.asElement());
|
|
686
|
+
|
|
687
|
+
// Ensure it's visible and clickable
|
|
688
|
+
await locator.waitFor({ state: 'visible', timeout: 10000 });
|
|
689
|
+
|
|
690
|
+
// Perform real mouse click
|
|
691
|
+
await locator.click({
|
|
692
|
+
force: true, // click even if slightly covered
|
|
693
|
+
timeout: 10000,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
console.log('Performed real mouse click on focused element');
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.error('Click failed on focused element:', error.message || error);
|
|
699
|
+
throw error; // fail the step if needed
|
|
389
700
|
}
|
|
390
701
|
}
|
|
702
|
+
function getNth(locator${anyType}, nthStr${stringType}) {
|
|
703
|
+
if (!nthStr) return locator;
|
|
704
|
+
const n = parseInt(nthStr, 10);
|
|
705
|
+
if (isNaN(n) || n < 1) return locator;
|
|
706
|
+
return locator.nth(n - 1); // k6 uses 0-based index
|
|
707
|
+
}
|
|
391
708
|
|
|
392
709
|
/**
|
|
393
|
-
* And I
|
|
710
|
+
* And I wait "X" seconds
|
|
394
711
|
*/
|
|
395
|
-
export function
|
|
396
|
-
|
|
397
|
-
|
|
712
|
+
export async function iWaitSeconds(page${anyType}, secondsStr${stringType}) {
|
|
713
|
+
const seconds = parseFloat(secondsStr);
|
|
714
|
+
if (isNaN(seconds) || seconds < 0) {
|
|
715
|
+
throw new Error(\`Invalid wait time: "\${secondsStr}"\`);
|
|
398
716
|
}
|
|
717
|
+
await sleep(seconds); // k6's sleep takes seconds
|
|
399
718
|
}
|
|
400
719
|
|
|
401
720
|
/**
|
|
402
|
-
*
|
|
721
|
+
* And I wait "X" milliseconds
|
|
403
722
|
*/
|
|
404
|
-
export function
|
|
405
|
-
const
|
|
406
|
-
|
|
723
|
+
export async function iWaitMilliseconds(page${anyType}, milliseconds${stringType}) {
|
|
724
|
+
const timeInMs = parseFloat(milliseconds);
|
|
725
|
+
if (isNaN(timeInMs) || timeInMs < 0) {
|
|
726
|
+
throw new Error(\`Invalid wait time: "\${milliseconds}" milliseconds\`);
|
|
727
|
+
}
|
|
728
|
+
console.log(\`Waiting for \${milliseconds} ms\`);
|
|
407
729
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
'has valid headers': (r) => r.request.headers['Authorization'] !== undefined
|
|
411
|
-
});
|
|
730
|
+
// Correct k6 browser method
|
|
731
|
+
await page.waitForTimeout(timeInMs);
|
|
412
732
|
}
|
|
413
733
|
|
|
414
734
|
/**
|
|
415
|
-
*
|
|
735
|
+
* And I find input element by placeholder text "..."
|
|
736
|
+
* Uses getByPlaceholder
|
|
416
737
|
*/
|
|
417
|
-
export function
|
|
418
|
-
const
|
|
738
|
+
export async function iFindInputElementByPlaceholderText(page${anyType}, placeholder${stringType}) {
|
|
739
|
+
const locator = page.getByPlaceholder(placeholder, { exact: false });
|
|
740
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
741
|
+
const count = await locator.count();
|
|
742
|
+
console.log(\`Found \${count} input(s) with placeholder "\${placeholder}"\`);
|
|
743
|
+
return locator;
|
|
419
744
|
}
|
|
420
745
|
|
|
421
|
-
|
|
422
|
-
|
|
746
|
+
/**
|
|
747
|
+
* And I find element by text "..."
|
|
748
|
+
* Uses getByText (substring match)
|
|
749
|
+
*/
|
|
750
|
+
export async function iFindElementByText(page${anyType}, text${stringType}) {
|
|
751
|
+
const locator = page.getByText(text, { exact: false });
|
|
752
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
753
|
+
const count = await locator.count();
|
|
754
|
+
console.log(\`Found \${count} element(s) containing text "\${text}"\`);
|
|
755
|
+
return locator;
|
|
423
756
|
}
|
|
424
757
|
|
|
425
758
|
/**
|
|
426
|
-
*
|
|
759
|
+
* And I find elements by text "..."
|
|
760
|
+
* Similar to above, but emphasizes multiple matches
|
|
427
761
|
*/
|
|
428
|
-
export function
|
|
429
|
-
|
|
762
|
+
export async function iFindElementsByText(page${anyType}, text${stringType}) {
|
|
763
|
+
const locator = page.getByText(text, { exact: false });
|
|
764
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
765
|
+
const count = await locator.count();
|
|
766
|
+
console.log(\`Found \${count} elements containing text "\${text}"\`);
|
|
767
|
+
return locator;
|
|
430
768
|
}
|
|
431
769
|
|
|
432
770
|
/**
|
|
433
|
-
*
|
|
771
|
+
* And I find button by text "..."
|
|
772
|
+
* Uses getByRole('button')
|
|
434
773
|
*/
|
|
435
|
-
export function
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
'POST status is 201': (r) => r.status === 201
|
|
442
|
-
});
|
|
774
|
+
export async function iFindButtonByText(page${anyType}, buttonText${stringType}) {
|
|
775
|
+
const locator = page.getByRole('button', { name: buttonText, exact: false });
|
|
776
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
777
|
+
const count = await locator.count();
|
|
778
|
+
console.log(\`Found \${count} button(s) with text "\${buttonText}"\`);
|
|
779
|
+
return locator;
|
|
443
780
|
}
|
|
444
|
-
|
|
781
|
+
/**
|
|
782
|
+
* And I find element by value "..."
|
|
783
|
+
* Finds input elements (input, textarea, select) that have the exact value attribute
|
|
784
|
+
* Example: And I find element by value "Submit"
|
|
785
|
+
*/
|
|
786
|
+
export async function iFindElementByValue(page${anyType}, valueText${stringType}) {
|
|
787
|
+
// Look for elements where value attribute matches exactly
|
|
788
|
+
const locator = page.locator(\`input[value="\${valueText}"], textarea[value="\${valueText}"]\`);
|
|
789
|
+
try {
|
|
790
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
791
|
+
const count = await locator.count();
|
|
792
|
+
|
|
793
|
+
if (count === 0) {
|
|
794
|
+
console.warn(\`No element found with value attribute "\${valueText}"\`);
|
|
795
|
+
// Optional fallback: check inner text or other attributes if needed
|
|
796
|
+
// locator = page.locator(\`: text("\${valueText}")\`);
|
|
797
|
+
}
|
|
445
798
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
799
|
+
console.log(\`Found \${ count } element(s) with value attribute "\${valueText}"\`);
|
|
800
|
+
|
|
801
|
+
// Optional: log tag names of matches
|
|
802
|
+
if (count > 0) {
|
|
803
|
+
const tags = await locator.evaluateAll(els => els.map(el => el.tagName.toLowerCase()));
|
|
804
|
+
console.log(\`Matching elements are: \${ tags.join(', ') } \`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return locator;
|
|
808
|
+
} catch (error) {
|
|
809
|
+
console.error(\`Could not find element by value "\${valueText}": \`, error.message || error);
|
|
810
|
+
check(false, { [\`Element with value "\${valueText}" is found\`]: false });
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
450
813
|
}
|
|
451
814
|
|
|
452
|
-
|
|
453
|
-
|
|
815
|
+
/**
|
|
816
|
+
* And I find element by role "..."
|
|
817
|
+
* Finds elements using ARIA role (button, textbox, link, heading, etc.)
|
|
818
|
+
* Example: And I find element by role "button"
|
|
819
|
+
* And I find element by role "heading" "Welcome"
|
|
820
|
+
*/
|
|
821
|
+
${isTS
|
|
822
|
+
? `export async function iFindElementByRole(page: any, roleName: string, nameOrOptions?: string | object) {`
|
|
823
|
+
: `export async function iFindElementByRole(page, roleName, nameOrOptions) {`}
|
|
824
|
+
let locator;
|
|
825
|
+
|
|
826
|
+
// If only role is given (no name)
|
|
827
|
+
if (!nameOrOptions) {
|
|
828
|
+
locator = page.getByRole(roleName);
|
|
829
|
+
}
|
|
830
|
+
// If role + name/text is given (second argument is string)
|
|
831
|
+
else if (typeof nameOrOptions === 'string') {
|
|
832
|
+
locator = page.getByRole(roleName, { name: nameOrOptions, exact: false });
|
|
833
|
+
}
|
|
834
|
+
// If options object is passed (advanced usage)
|
|
835
|
+
else {
|
|
836
|
+
locator = page.getByRole(roleName, nameOrOptions);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
try {
|
|
840
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
841
|
+
const count = await locator.count();
|
|
842
|
+
console.log(\`Found \${ count } element(s) by role "\${roleName}"\`);
|
|
843
|
+
|
|
844
|
+
if (typeof nameOrOptions === 'string') {
|
|
845
|
+
console.log(\` with name containing "\${nameOrOptions}"\`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return locator;
|
|
849
|
+
} catch (error) {
|
|
850
|
+
console.error(\`Could not find element by role "\${roleName}": \`, error.message || error);
|
|
851
|
+
check(false, { [\`Element by role "\${roleName}" is found\`]: false });
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
454
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* And I find buttons by text "..."
|
|
857
|
+
* Same as above, but name emphasizes multiple
|
|
858
|
+
*/
|
|
859
|
+
export async function iFindButtonsByText(page${anyType}, buttonText${stringType}) {
|
|
860
|
+
const locator = page.getByRole('button', { name: buttonText, exact: false });
|
|
861
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
862
|
+
const count = await locator.count();
|
|
863
|
+
console.log(\`Found \${count} button(s) with text "\${buttonText}"\`);
|
|
864
|
+
return locator;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* And I find element by name "..."
|
|
868
|
+
* Finds elements using getByRole(name) or [name="..."] attribute
|
|
869
|
+
* Example: And I find element by name "username"
|
|
870
|
+
*/
|
|
871
|
+
export async function iFindElementByName(page${anyType}, labelText${stringType}) {
|
|
872
|
+
const locator = page.getByLabel(labelText, { exact: false });
|
|
873
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
874
|
+
console.log(\`Found input by label: "\${labelText}"\`);
|
|
875
|
+
return locator;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* And I find element by Id "..."
|
|
879
|
+
* Uses locator('#id')
|
|
880
|
+
*/
|
|
881
|
+
export async function iFindElementById(page${anyType}, id${stringType}) {
|
|
882
|
+
// Ensure ID starts with # if user forgets
|
|
883
|
+
const selector = id.startsWith('#') ? id : \`#\${id}\`;
|
|
884
|
+
const locator = page.locator(selector);
|
|
885
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
886
|
+
console.log(\`Found element by ID: \${selector}\`);
|
|
887
|
+
return locator;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* And I click "N"st element by selector "..."
|
|
891
|
+
* Example: And I click "1"st element by selector ".btn"
|
|
892
|
+
*/
|
|
893
|
+
export async function iClickNthElementBySelector(page${anyType}, n${stringType}, selector${stringType}) {
|
|
894
|
+
const index = parseInt(n.replace(/\D/g, '')) - 1; // "1"st → 0, "2"nd → 1, etc.
|
|
895
|
+
if (isNaN(index)) throw new Error(\`Invalid nth value: \${ n } \`);
|
|
896
|
+
|
|
897
|
+
const locator = page.locator(selector).nth(index);
|
|
898
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
899
|
+
await locator.click();
|
|
900
|
+
console.log(\`Clicked the \${ n } element matching selector: \${ selector } \`);
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* And I click on exact text "..."
|
|
904
|
+
* Clicks the first element that **exactly matches** the given visible text.
|
|
905
|
+
* Uses page.getByText(..., { exact: true })
|
|
906
|
+
*/
|
|
907
|
+
export async function iClickOnExactText(page${anyType}, text${stringType}) {
|
|
908
|
+
// Use exact match to avoid partial matches like "Submit Form" when you want "Submit"
|
|
909
|
+
const locator = page.getByText(text, { exact: true });
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
await locator.waitFor({ state: 'visible', timeout: 20000 });
|
|
913
|
+
const count = await locator.count();
|
|
914
|
+
if (count === 0) {
|
|
915
|
+
throw new Error(\`No element found with exact text: "\${text}"\`);
|
|
916
|
+
}
|
|
917
|
+
if (count > 1) {
|
|
918
|
+
console.warn(\`⚠️ Multiple (\${count}) elements found with exact text "\${text}". Clicking the first.\`);
|
|
919
|
+
}
|
|
455
920
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
921
|
+
await locator.first().click(); // safest: click first even if multiple
|
|
922
|
+
console.log(\`✅ Clicked element with exact text: "\${text}"\`);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
console.error(\`Failed to click exact text "\${text}":\`, error.message || error);
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* And I fill the "N"rd field element by selector "..." with "..."
|
|
930
|
+
* Example: And I fill the "3"rd field element by selector "input" with "08012345678"
|
|
931
|
+
*/
|
|
932
|
+
export async function iFillNthFieldBySelector(page${anyType}, n${stringType}, selector${stringType}, value${stringType}) {
|
|
933
|
+
const index = parseInt(n.replace(/\D/g, '')) - 1;
|
|
934
|
+
if (isNaN(index)) throw new Error(\`Invalid nth value: \${ n } \`);
|
|
935
|
+
|
|
936
|
+
const locator = page.locator(selector).nth(index);
|
|
937
|
+
await locator.waitFor({ state: 'visible', timeout: 15000 });
|
|
938
|
+
await locator.fill(value);
|
|
939
|
+
console.log(\`Filled the \${ n } element matching "\${selector}" with "\${value}"\`);
|
|
461
940
|
}
|
|
462
941
|
`;
|
|
463
942
|
fs_1.default.writeFileSync(path_1.default.join(outputPath, "steps", `sample.steps${stepExtension}`), sampleSteps);
|
|
@@ -487,7 +966,7 @@ coverage/
|
|
|
487
966
|
forceConsistentCasingInFileNames: true,
|
|
488
967
|
types: ["k6"],
|
|
489
968
|
},
|
|
490
|
-
include: ["src/**/*", "steps/**/*"],
|
|
969
|
+
include: ["src/**/*", "steps/**/*", "**/*.ts"],
|
|
491
970
|
exclude: ["node_modules"],
|
|
492
971
|
};
|
|
493
972
|
fs_1.default.writeFileSync(path_1.default.join(outputPath, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|