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.
@@ -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
- - \`features/\` - Gherkin feature files
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 = `@browser
167
- Feature: Browser Performance Example
168
- # Scenarios with @browser tag run in a real Chromium instance
169
-
170
- Scenario: Verify UI Elements
171
- Given the base URL is "https://test.k6.io"
172
- When I navigate to the "/" page
173
- Then I see the text on the page "Collection of simple web-pages"
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
- * Given the base URL is "..."
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
- /** * IMPORTANT: For k6 to auto-encode, the body MUST be a plain object.
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
- try {
257
- const parsed = response.json();
258
- globalThis.lastResponse = parsed;
259
- console.log(\`✅ \${context} Response Captured. Keys: \${Object.keys(parsed).join(', ')}\`);
260
- } catch (e) {
261
- console.error(\`❌ Failed to parse JSON response for \${context}: \${response.body}\`);
262
- globalThis.lastResponse = null;
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
- /** * IMPORTANT: For k6 to auto-encode, the body MUST be a plain object.
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
- try {
326
- const parsed = response.json();
327
- globalThis.lastResponse = parsed;
328
- console.log(\`✅ \${context} Response Captured. Keys: \${Object.keys(parsed).join(', ')}\`);
329
- } catch (e) {
330
- console.error(\`❌ Failed to parse JSON response for \${context}: \${response.body}\`);
331
- globalThis.lastResponse = null;
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 "data.token" in "data/standard_user.json"
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${stringType}, fileName${stringType}) {
426
+ export function iStoreIn(jsonPath, fileName) {
343
427
  const responseData = globalThis.lastResponse;
344
428
 
345
- if (!responseData) return;
346
- const value = jsonPath.split('.').reduce((acc, key) => acc && acc[key], responseData);
347
-
429
+ if (!responseData) {
430
+ console.error('❌ No response data to store. Did an HTTP request run?');
431
+ return;
432
+ }
348
433
 
349
- // Debug: See what the server actually sent back
350
- console.log(\`DEBUG: Response for \${fileName}: \`, JSON.stringify(responseData));
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
- // Check for undefined/null specifically so 0 or "" aren't ignored
355
- if (value !== undefined && value !== null) {
356
- globalThis.savedTokens = globalThis.savedTokens || {};
357
-
358
- // Store in the requested file path
359
- globalThis.savedTokens[fileName] = value;
360
-
361
- // ALSO store as a clean alias (e.g., 'service_account' instead of 'data/service_account.json')
362
- const alias = fileName.split('/').pop().replace('.json', '');
363
- globalThis.savedTokens[alias] = value;
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(\`✅ Value staged for file write: \${fileName}\`); } else {
366
- console.error(\`❌ Could not find path "\${jsonPath}" in the response. JSON keys found: \${Object.keys(responseData).join(', ')}\`);
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
- * Background Auth: Applies the previously stored token to headers
648
+ * And I find element by textarea "..."
649
+ * (uses getByPlaceholder or getByRole for textarea)
371
650
  */
372
- export function iAmAuthenticatedAsA(userType${stringType}) {
373
- const memoryKey = \`data/\${userType}.json\`;
374
- const token = globalThis.savedTokens && globalThis.savedTokens[memoryKey];
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 (token) {
377
- defaultHeaders['Authorization'] = \`Bearer \${token}\`;
378
- console.log(\`Using memory-stored token for \${userType}\`);
379
- } else {
380
- // Fallback: try to read the INITIAL file created during project init
381
- try {
382
- const userData = JSON.parse(open(\`../data/\${userType}.json\`));
383
- if (userData.token) {
384
- defaultHeaders['Authorization'] = \`Bearer \${userData.token}\`;
385
- }
386
- } catch (e) {
387
- console.warn(\`No token found for \${userType} in memory or file.\`);
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 set the default headers:
710
+ * And I wait "X" seconds
394
711
  */
395
- export function iSetTheDefaultHeaders(data${anyType}) {
396
- if (data && data.length > 0) {
397
- Object.assign(defaultHeaders, data[0]);
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
- * When I make a GET request to "..."
721
+ * And I wait "X" milliseconds
403
722
  */
404
- export function iMakeAGetRequestTo(endpoint${stringType}) {
405
- const url = \`\${baseUrl}\${endpoint}\`;
406
- const response = http.get(url, { headers: defaultHeaders });
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
- check(response, {
409
- 'status is 200 or 404': (r) => r.status === 200 || r.status === 404,
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
- * Then the response status should be ...
735
+ * And I find input element by placeholder text "..."
736
+ * Uses getByPlaceholder
416
737
  */
417
- export function theResponseStatusShouldBe(expectedStatus${mixedType}) {
418
- const status = typeof expectedStatus === 'string' ? parseInt(expectedStatus) : expectedStatus;
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
- export function theResponseShouldContain(field${stringType}) {
422
- console.log(\`Verified: Response contains \${field}\`);
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
- * Given I have the following post data:
759
+ * And I find elements by text "..."
760
+ * Similar to above, but emphasizes multiple matches
427
761
  */
428
- export function iHaveTheFollowingPostData(content${stringType}) {
429
- return JSON.parse(content);
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
- * When I make a POST request to "..."
771
+ * And I find button by text "..."
772
+ * Uses getByRole('button')
434
773
  */
435
- export function iMakeAPostRequestTo(endpoint${stringType}) {
436
- const url = \`\${baseUrl}\${endpoint}\`;
437
- const payload = JSON.stringify({ title: 'k6 test' });
438
- const response = http.post(url, payload, { headers: defaultHeaders });
439
-
440
- check(response, {
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
- /** --- Browser Steps (@browser) --- **/
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
- export async function iNavigateToThePage(page, url${stringType}) {
447
- // If URL is relative, prepend baseUrl
448
- const fullUrl = url.startsWith('http') ? url : \`\${baseUrl}\${url}\`;
449
- await page.goto(fullUrl);
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
- export async function iClickTheButton(page, selector${stringType}) {
453
- await page.locator(selector).click();
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
- export async function iSeeTheTextOnThePage(page, text${stringType}) {
457
- const content = await page.content();
458
- check(page, {
459
- [\`Text "\${text}" is visible\`]: () => content.includes(text)
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));