k6-cucumber-steps 2.0.0 → 2.0.2

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