k6-cucumber-steps 2.0.1 → 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.
- 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 +620 -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",
|
|
@@ -83,8 +97,8 @@ npm test
|
|
|
83
97
|
\`\`\`
|
|
84
98
|
|
|
85
99
|
## Project Structure
|
|
86
|
-
- \`
|
|
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 = `@
|
|
167
|
-
Feature:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 "
|
|
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
|
|
425
|
+
export function iStoreIn(jsonPath, fileName) {
|
|
343
426
|
const responseData = globalThis.lastResponse;
|
|
344
427
|
|
|
345
|
-
if (!responseData)
|
|
346
|
-
|
|
347
|
-
|
|
428
|
+
if (!responseData) {
|
|
429
|
+
console.error('❌ No response data to store. Did an HTTP request run?');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
348
432
|
|
|
349
|
-
//
|
|
350
|
-
|
|
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
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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(
|
|
366
|
-
|
|
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
|
-
*
|
|
647
|
+
* And I find element by textarea "..."
|
|
648
|
+
* (uses getByPlaceholder or getByRole for textarea)
|
|
371
649
|
*/
|
|
372
|
-
export function
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
709
|
+
* And I wait "X" seconds
|
|
394
710
|
*/
|
|
395
|
-
export function
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
*
|
|
720
|
+
* And I wait "X" milliseconds
|
|
403
721
|
*/
|
|
404
|
-
export function
|
|
405
|
-
const
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
*
|
|
734
|
+
* And I find input element by placeholder text "..."
|
|
735
|
+
* Uses getByPlaceholder
|
|
416
736
|
*/
|
|
417
|
-
export function
|
|
418
|
-
const
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
*
|
|
758
|
+
* And I find elements by text "..."
|
|
759
|
+
* Similar to above, but emphasizes multiple matches
|
|
427
760
|
*/
|
|
428
|
-
export function
|
|
429
|
-
|
|
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
|
-
*
|
|
770
|
+
* And I find button by text "..."
|
|
771
|
+
* Uses getByRole('button')
|
|
434
772
|
*/
|
|
435
|
-
export function
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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));
|