@qate/cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +449 -449
- package/dist/AuthFlowExecutor.d.ts +87 -0
- package/dist/AuthFlowExecutor.d.ts.map +1 -0
- package/dist/AuthFlowExecutor.js +351 -0
- package/dist/AuthFlowExecutor.js.map +1 -0
- package/dist/AxiosGenerator.js +28 -28
- package/dist/JunitXmlGenerator.d.ts +12 -0
- package/dist/JunitXmlGenerator.d.ts.map +1 -0
- package/dist/JunitXmlGenerator.js +114 -0
- package/dist/JunitXmlGenerator.js.map +1 -0
- package/dist/PlaywrightGenerator.d.ts +1 -1
- package/dist/PlaywrightGenerator.d.ts.map +1 -1
- package/dist/PlaywrightGenerator.js +796 -758
- package/dist/PlaywrightGenerator.js.map +1 -1
- package/dist/RestApiExecutor.d.ts +3 -4
- package/dist/RestApiExecutor.d.ts.map +1 -1
- package/dist/RestApiExecutor.js +63 -3
- package/dist/RestApiExecutor.js.map +1 -1
- package/dist/cli.js +536 -148
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +78 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +47 -6
- package/dist/client.js.map +1 -1
- package/package.json +71 -71
|
@@ -81,7 +81,7 @@ function getActionCode(toolName, input, indent, usePlaceholderResolve) {
|
|
|
81
81
|
case 'go_to':
|
|
82
82
|
return `${indent}await page.goto(${wrapValue(input.url, usePlaceholderResolve)});`;
|
|
83
83
|
case 'click':
|
|
84
|
-
return `${indent}await page.click('${escapeString(input.selector)}')
|
|
84
|
+
return `${indent}try {\n${indent} await page.click('${escapeString(input.selector)}');\n${indent}} catch (e) {\n${indent} if (e.message?.includes('intercepts pointer events')) {\n${indent} await page.locator('${escapeString(input.selector)}').evaluate(el => el.click());\n${indent} } else { throw e; }\n${indent}}`;
|
|
85
85
|
case 'fill':
|
|
86
86
|
return `${indent}await page.fill('${escapeString(input.selector)}', ${wrapValue(input.value, usePlaceholderResolve)});`;
|
|
87
87
|
case 'type':
|
|
@@ -120,21 +120,21 @@ function getActionCode(toolName, input, indent, usePlaceholderResolve) {
|
|
|
120
120
|
function wrapWithTiming(actionCode, stepId, stepIndex, indent, description) {
|
|
121
121
|
const varPrefix = `step_${stepIndex}`;
|
|
122
122
|
const escapedDescription = escapeString(description);
|
|
123
|
-
return `${indent}// Step ${stepIndex + 1}: ${stepId}
|
|
124
|
-
${indent}qateStartStepNetworkCapture();
|
|
125
|
-
${indent}const ${varPrefix}_activityStart = Date.now();
|
|
126
|
-
${indent}let ${varPrefix}_error: string | undefined;
|
|
127
|
-
${indent}try {
|
|
128
|
-
${actionCode.split('\n').map(line => indent + ' ' + line.trim()).join('\n')}
|
|
129
|
-
${indent}} catch (e) {
|
|
130
|
-
${indent} ${varPrefix}_error = e instanceof Error ? e.message : String(e);
|
|
131
|
-
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
132
|
-
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'failed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd, ${varPrefix}_error);
|
|
133
|
-
${indent} throw e;
|
|
134
|
-
${indent}}
|
|
135
|
-
${indent}const ${varPrefix}_activityEnd = Date.now();
|
|
136
|
-
${indent}await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
137
|
-
${indent}const ${varPrefix}_settledEnd = Date.now();
|
|
123
|
+
return `${indent}// Step ${stepIndex + 1}: ${stepId}
|
|
124
|
+
${indent}qateStartStepNetworkCapture();
|
|
125
|
+
${indent}const ${varPrefix}_activityStart = Date.now();
|
|
126
|
+
${indent}let ${varPrefix}_error: string | undefined;
|
|
127
|
+
${indent}try {
|
|
128
|
+
${actionCode.split('\n').map(line => indent + ' ' + line.trim()).join('\n')}
|
|
129
|
+
${indent}} catch (e) {
|
|
130
|
+
${indent} ${varPrefix}_error = e instanceof Error ? e.message : String(e);
|
|
131
|
+
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
132
|
+
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'failed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd, ${varPrefix}_error);
|
|
133
|
+
${indent} throw e;
|
|
134
|
+
${indent}}
|
|
135
|
+
${indent}const ${varPrefix}_activityEnd = Date.now();
|
|
136
|
+
${indent}await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
137
|
+
${indent}const ${varPrefix}_settledEnd = Date.now();
|
|
138
138
|
${indent}qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'passed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_settledEnd);`;
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
@@ -144,18 +144,18 @@ ${indent}qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}'
|
|
|
144
144
|
function wrapWithStepTracking(actionCode, stepId, stepIndex, indent, description) {
|
|
145
145
|
const varPrefix = `step_${stepIndex}`;
|
|
146
146
|
const escapedDescription = escapeString(description);
|
|
147
|
-
return `${indent}// Step ${stepIndex + 1}: ${stepId}
|
|
148
|
-
${indent}qateStartStepNetworkCapture();
|
|
149
|
-
${indent}const ${varPrefix}_activityStart = Date.now();
|
|
150
|
-
${indent}try {
|
|
151
|
-
${actionCode.split('\n').map(line => indent + ' ' + line.trim()).join('\n')}
|
|
152
|
-
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
153
|
-
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'passed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd);
|
|
154
|
-
${indent}} catch (e) {
|
|
155
|
-
${indent} const ${varPrefix}_error = e instanceof Error ? e.message : String(e);
|
|
156
|
-
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
157
|
-
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'failed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd, ${varPrefix}_error);
|
|
158
|
-
${indent} throw e;
|
|
147
|
+
return `${indent}// Step ${stepIndex + 1}: ${stepId}
|
|
148
|
+
${indent}qateStartStepNetworkCapture();
|
|
149
|
+
${indent}const ${varPrefix}_activityStart = Date.now();
|
|
150
|
+
${indent}try {
|
|
151
|
+
${actionCode.split('\n').map(line => indent + ' ' + line.trim()).join('\n')}
|
|
152
|
+
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
153
|
+
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'passed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd);
|
|
154
|
+
${indent}} catch (e) {
|
|
155
|
+
${indent} const ${varPrefix}_error = e instanceof Error ? e.message : String(e);
|
|
156
|
+
${indent} const ${varPrefix}_activityEnd = Date.now();
|
|
157
|
+
${indent} qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'failed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_activityEnd, ${varPrefix}_error);
|
|
158
|
+
${indent} throw e;
|
|
159
159
|
${indent}}`;
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
@@ -193,9 +193,9 @@ function stepToPlaywright(step, indent = ' ', usePlaceholderResolve = false,
|
|
|
193
193
|
case 'scroll':
|
|
194
194
|
const direction = input.direction || 'down';
|
|
195
195
|
const multiplier = input.multiplier || 0.7;
|
|
196
|
-
actionCode = `await page.evaluate(({ dir, mult }) => {
|
|
197
|
-
const scrollAmount = window.innerHeight * mult;
|
|
198
|
-
window.scrollBy(0, dir === 'down' ? scrollAmount : -scrollAmount);
|
|
196
|
+
actionCode = `await page.evaluate(({ dir, mult }) => {
|
|
197
|
+
const scrollAmount = window.innerHeight * mult;
|
|
198
|
+
window.scrollBy(0, dir === 'down' ? scrollAmount : -scrollAmount);
|
|
199
199
|
}, { dir: '${direction}', mult: ${multiplier} });`;
|
|
200
200
|
break;
|
|
201
201
|
case 'waitForSelector':
|
|
@@ -347,41 +347,41 @@ function generateTestFile(test, baseUrl) {
|
|
|
347
347
|
authRoleId: test.authRoleId || null,
|
|
348
348
|
placeholders: placeholders,
|
|
349
349
|
};
|
|
350
|
-
contextSetup = `
|
|
351
|
-
// Resolve placeholders at runtime
|
|
352
|
-
const testContext = ${JSON.stringify(testContext, null, 6).replace(/\n/g, '\n ')};
|
|
353
|
-
const resolved = await qateResolve(testContext);
|
|
354
|
-
const resolve = (v: string) => replacePlaceholders(v, resolved);
|
|
350
|
+
contextSetup = `
|
|
351
|
+
// Resolve placeholders at runtime
|
|
352
|
+
const testContext = ${JSON.stringify(testContext, null, 6).replace(/\n/g, '\n ')};
|
|
353
|
+
const resolved = await qateResolve(testContext);
|
|
354
|
+
const resolve = (v: string) => replacePlaceholders(v, resolved);
|
|
355
355
|
`;
|
|
356
356
|
}
|
|
357
|
-
return `/**
|
|
358
|
-
* Test: ${test.name}${description}${tags}${priority}
|
|
359
|
-
*
|
|
360
|
-
* Generated by Qate CLI
|
|
361
|
-
* Do not edit manually - regenerate using: qate generate
|
|
362
|
-
*/
|
|
363
|
-
|
|
364
|
-
${imports.join('\n')}
|
|
365
|
-
|
|
366
|
-
test.describe('${escapeString(test.name)}', () => {
|
|
367
|
-
test('${escapeString(test.name)}', async ({ page }, testInfo) => {
|
|
368
|
-
// Set up network capture (only captures when QATE_EXECUTION_ID is set)
|
|
369
|
-
qateSetupNetworkCapture(page);
|
|
370
|
-
${contextSetup}
|
|
371
|
-
try {
|
|
372
|
-
${steps.split('\n').map(line => ' ' + line).join('\n')}
|
|
373
|
-
} finally {
|
|
374
|
-
// Always attach step timings for reporter (even on failure)
|
|
375
|
-
const stepTimings = qateGetAndClearStepTimings();
|
|
376
|
-
if (stepTimings.length > 0) {
|
|
377
|
-
await testInfo.attach('qate-step-timings', {
|
|
378
|
-
body: JSON.stringify(stepTimings),
|
|
379
|
-
contentType: 'application/json',
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
});
|
|
357
|
+
return `/**
|
|
358
|
+
* Test: ${test.name}${description}${tags}${priority}
|
|
359
|
+
*
|
|
360
|
+
* Generated by Qate CLI
|
|
361
|
+
* Do not edit manually - regenerate using: qate generate
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
${imports.join('\n')}
|
|
365
|
+
|
|
366
|
+
test.describe('${escapeString(test.name)}', () => {
|
|
367
|
+
test('${escapeString(test.name)}', async ({ page }, testInfo) => {
|
|
368
|
+
// Set up network capture (only captures when QATE_EXECUTION_ID is set)
|
|
369
|
+
qateSetupNetworkCapture(page);
|
|
370
|
+
${contextSetup}
|
|
371
|
+
try {
|
|
372
|
+
${steps.split('\n').map(line => ' ' + line).join('\n')}
|
|
373
|
+
} finally {
|
|
374
|
+
// Always attach step timings for reporter (even on failure)
|
|
375
|
+
const stepTimings = qateGetAndClearStepTimings();
|
|
376
|
+
if (stepTimings.length > 0) {
|
|
377
|
+
await testInfo.attach('qate-step-timings', {
|
|
378
|
+
body: JSON.stringify(stepTimings),
|
|
379
|
+
contentType: 'application/json',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
385
|
`;
|
|
386
386
|
}
|
|
387
387
|
/**
|
|
@@ -446,7 +446,7 @@ function toLambdaTestPlatform(os, osVersion) {
|
|
|
446
446
|
* Generate playwright.config.ts
|
|
447
447
|
*/
|
|
448
448
|
function generateConfig(options, appUrl) {
|
|
449
|
-
const { provider = 'local',
|
|
449
|
+
const { provider = 'local', browser = 'chromium', browserVersion = 'latest', os = 'windows', osVersion = '11', orchestration = 'websocket', device } = options;
|
|
450
450
|
let projects = '';
|
|
451
451
|
let connectOptions = '';
|
|
452
452
|
// Determine effective orchestration mode
|
|
@@ -455,120 +455,134 @@ function generateConfig(options, appUrl) {
|
|
|
455
455
|
if (provider === 'browserstack' && effectiveOrchestration === 'websocket') {
|
|
456
456
|
const bsBrowser = toBrowserStackBrowser(browser);
|
|
457
457
|
const bsOS = toBrowserStackOS(os);
|
|
458
|
-
connectOptions = `
|
|
459
|
-
// BrowserStack configuration (WebSocket/CDP approach)
|
|
460
|
-
// Docs: https://www.browserstack.com/docs/automate/playwright
|
|
461
|
-
use: {
|
|
462
|
-
connectOptions: {
|
|
463
|
-
wsEndpoint: \`wss://cdp.browserstack.com/playwright?caps=\${encodeURIComponent(JSON.stringify({
|
|
464
|
-
'browser': '${bsBrowser}',
|
|
465
|
-
'browser_version': '${browserVersion}',
|
|
466
|
-
'os': '${bsOS}',
|
|
467
|
-
'os_version': '${osVersion}',
|
|
468
|
-
'project': 'Qate Tests',
|
|
469
|
-
'build': process.env.BUILD_NUMBER || 'local',
|
|
470
|
-
'name': 'Playwright Test',
|
|
471
|
-
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
|
|
472
|
-
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,
|
|
473
|
-
'browserstack.debug': true,
|
|
474
|
-
'browserstack.console': 'verbose',
|
|
475
|
-
'browserstack.networkLogs': true,
|
|
476
|
-
}))}\`,
|
|
477
|
-
},
|
|
458
|
+
connectOptions = `
|
|
459
|
+
// BrowserStack configuration (WebSocket/CDP approach)
|
|
460
|
+
// Docs: https://www.browserstack.com/docs/automate/playwright
|
|
461
|
+
use: {
|
|
462
|
+
connectOptions: {
|
|
463
|
+
wsEndpoint: \`wss://cdp.browserstack.com/playwright?caps=\${encodeURIComponent(JSON.stringify({
|
|
464
|
+
'browser': '${bsBrowser}',
|
|
465
|
+
'browser_version': '${browserVersion}',
|
|
466
|
+
'os': '${bsOS}',
|
|
467
|
+
'os_version': '${osVersion}',
|
|
468
|
+
'project': 'Qate Tests',
|
|
469
|
+
'build': process.env.BUILD_NUMBER || 'local',
|
|
470
|
+
'name': 'Playwright Test',
|
|
471
|
+
'browserstack.username': process.env.BROWSERSTACK_USERNAME,
|
|
472
|
+
'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY,
|
|
473
|
+
'browserstack.debug': true,
|
|
474
|
+
'browserstack.console': 'verbose',
|
|
475
|
+
'browserstack.networkLogs': true,
|
|
476
|
+
}))}\`,
|
|
477
|
+
},
|
|
478
478
|
},`;
|
|
479
479
|
}
|
|
480
480
|
else if (provider === 'browserstack' && effectiveOrchestration === 'cli') {
|
|
481
481
|
// BrowserStack CLI orchestration - no connectOptions needed
|
|
482
|
-
connectOptions = `
|
|
483
|
-
// BrowserStack configuration (CLI orchestration)
|
|
484
|
-
// Run with: npx browserstack-node-sdk playwright test
|
|
482
|
+
connectOptions = `
|
|
483
|
+
// BrowserStack configuration (CLI orchestration)
|
|
484
|
+
// Run with: npx browserstack-node-sdk playwright test
|
|
485
485
|
// Docs: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs`;
|
|
486
486
|
}
|
|
487
487
|
else if (provider === 'saucelabs') {
|
|
488
488
|
// Sauce Labs only supports CLI orchestration
|
|
489
|
-
connectOptions = `
|
|
490
|
-
// Sauce Labs configuration (CLI orchestration only)
|
|
491
|
-
// IMPORTANT: Sauce Labs requires the saucectl CLI to run tests
|
|
492
|
-
// Install: npm install -g saucectl
|
|
493
|
-
// Run: saucectl run
|
|
489
|
+
connectOptions = `
|
|
490
|
+
// Sauce Labs configuration (CLI orchestration only)
|
|
491
|
+
// IMPORTANT: Sauce Labs requires the saucectl CLI to run tests
|
|
492
|
+
// Install: npm install -g saucectl
|
|
493
|
+
// Run: saucectl run
|
|
494
494
|
// Docs: https://docs.saucelabs.com/web-apps/automated-testing/playwright/`;
|
|
495
495
|
}
|
|
496
496
|
else if (provider === 'lambdatest' && effectiveOrchestration === 'websocket') {
|
|
497
497
|
const ltBrowser = toLambdaTestBrowser(browser);
|
|
498
498
|
const ltPlatform = toLambdaTestPlatform(os, osVersion);
|
|
499
|
-
connectOptions = `
|
|
500
|
-
// LambdaTest configuration (WebSocket/CDP approach)
|
|
501
|
-
// Docs: https://www.lambdatest.com/support/docs/playwright-testing/
|
|
502
|
-
use: {
|
|
503
|
-
connectOptions: {
|
|
504
|
-
wsEndpoint: \`wss://cdp.lambdatest.com/playwright?capabilities=\${encodeURIComponent(JSON.stringify({
|
|
505
|
-
'browserName': '${ltBrowser}',
|
|
506
|
-
'browserVersion': '${browserVersion}',
|
|
507
|
-
'LT:Options': {
|
|
508
|
-
'platform': '${ltPlatform}',
|
|
509
|
-
'build': process.env.BUILD_NUMBER || 'Qate Tests',
|
|
510
|
-
'name': 'Qate Playwright Test',
|
|
511
|
-
'user': process.env.LT_USERNAME,
|
|
512
|
-
'accessKey': process.env.LT_ACCESS_KEY,
|
|
513
|
-
'network': true,
|
|
514
|
-
'video': true,
|
|
515
|
-
'console': true,
|
|
516
|
-
}
|
|
517
|
-
}))}\`,
|
|
518
|
-
},
|
|
499
|
+
connectOptions = `
|
|
500
|
+
// LambdaTest configuration (WebSocket/CDP approach)
|
|
501
|
+
// Docs: https://www.lambdatest.com/support/docs/playwright-testing/
|
|
502
|
+
use: {
|
|
503
|
+
connectOptions: {
|
|
504
|
+
wsEndpoint: \`wss://cdp.lambdatest.com/playwright?capabilities=\${encodeURIComponent(JSON.stringify({
|
|
505
|
+
'browserName': '${ltBrowser}',
|
|
506
|
+
'browserVersion': '${browserVersion}',
|
|
507
|
+
'LT:Options': {
|
|
508
|
+
'platform': '${ltPlatform}',
|
|
509
|
+
'build': process.env.BUILD_NUMBER || 'Qate Tests',
|
|
510
|
+
'name': 'Qate Playwright Test',
|
|
511
|
+
'user': process.env.LT_USERNAME,
|
|
512
|
+
'accessKey': process.env.LT_ACCESS_KEY,
|
|
513
|
+
'network': true,
|
|
514
|
+
'video': true,
|
|
515
|
+
'console': true,
|
|
516
|
+
}
|
|
517
|
+
}))}\`,
|
|
518
|
+
},
|
|
519
519
|
},`;
|
|
520
520
|
}
|
|
521
521
|
else if (provider === 'lambdatest' && effectiveOrchestration === 'cli') {
|
|
522
522
|
// LambdaTest CLI orchestration - no connectOptions needed
|
|
523
|
-
connectOptions = `
|
|
524
|
-
// LambdaTest HyperExecute configuration (CLI orchestration)
|
|
525
|
-
// Run with: ./hyperexecute --config hyperexecute.yaml
|
|
523
|
+
connectOptions = `
|
|
524
|
+
// LambdaTest HyperExecute configuration (CLI orchestration)
|
|
525
|
+
// Run with: ./hyperexecute --config hyperexecute.yaml
|
|
526
526
|
// Docs: https://www.lambdatest.com/support/docs/hyperexecute-guided-walkthrough/`;
|
|
527
527
|
}
|
|
528
528
|
else {
|
|
529
529
|
// Local provider
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
530
|
+
const deviceName = device && device !== 'Desktop' ? device : null;
|
|
531
|
+
let browserProject;
|
|
532
|
+
if (deviceName) {
|
|
533
|
+
browserProject = `
|
|
534
|
+
{
|
|
535
|
+
name: '${browser}',
|
|
536
|
+
use: { ...devices['${deviceName}'] },
|
|
537
|
+
}`;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Map browser name to default Playwright device
|
|
541
|
+
const defaultDevice = browser === 'firefox' ? 'Desktop Firefox' : browser === 'webkit' ? 'Desktop Safari' : 'Desktop Chrome';
|
|
542
|
+
browserProject = `
|
|
543
|
+
{
|
|
544
|
+
name: '${browser}',
|
|
545
|
+
use: { ...devices['${defaultDevice}'] },
|
|
546
|
+
}`;
|
|
547
|
+
}
|
|
548
|
+
projects = `
|
|
549
|
+
projects: [${browserProject}
|
|
537
550
|
],`;
|
|
538
551
|
}
|
|
539
|
-
return `/**
|
|
540
|
-
* Playwright Configuration
|
|
541
|
-
* Generated by Qate CLI
|
|
542
|
-
*
|
|
543
|
-
* Provider: ${provider}
|
|
544
|
-
*/
|
|
545
|
-
|
|
546
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
547
|
-
|
|
548
|
-
export default defineConfig({
|
|
549
|
-
testDir: './tests',
|
|
550
|
-
fullyParallel: true,
|
|
551
|
-
forbidOnly: !!process.env.CI,
|
|
552
|
-
retries: process.env.CI ? 2 : 0,
|
|
553
|
-
workers: process.env.CI ? 1 : undefined,
|
|
554
|
-
reporter: [
|
|
555
|
-
['html'],
|
|
556
|
-
['json', { outputFile: 'test-results.json' }],
|
|
557
|
-
['
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
timeout
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
552
|
+
return `/**
|
|
553
|
+
* Playwright Configuration
|
|
554
|
+
* Generated by Qate CLI
|
|
555
|
+
*
|
|
556
|
+
* Provider: ${provider}
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
560
|
+
|
|
561
|
+
export default defineConfig({
|
|
562
|
+
testDir: './tests',
|
|
563
|
+
fullyParallel: true,
|
|
564
|
+
forbidOnly: !!process.env.CI,
|
|
565
|
+
retries: process.env.CI ? 2 : 0,
|
|
566
|
+
workers: process.env.CI ? 1 : undefined,
|
|
567
|
+
reporter: [
|
|
568
|
+
['html'],
|
|
569
|
+
['json', { outputFile: 'test-results.json' }],
|
|
570
|
+
['junit', { outputFile: 'results/junit.xml' }],
|
|
571
|
+
['./qate-reporter.ts'],
|
|
572
|
+
],
|
|
573
|
+
|
|
574
|
+
use: {
|
|
575
|
+
baseURL: '${appUrl}',
|
|
576
|
+
trace: 'on-first-retry',
|
|
577
|
+
screenshot: 'only-on-failure',
|
|
578
|
+
},
|
|
579
|
+
${connectOptions}${projects}
|
|
580
|
+
// Global timeout
|
|
581
|
+
timeout: 30000,
|
|
582
|
+
expect: {
|
|
583
|
+
timeout: 5000,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
572
586
|
`;
|
|
573
587
|
}
|
|
574
588
|
/**
|
|
@@ -606,244 +620,265 @@ function generateQateReporter(testMapping, options) {
|
|
|
606
620
|
const executionId = options.executionId || '';
|
|
607
621
|
const apiUrl = options.apiUrl || 'https://api.qate.ai';
|
|
608
622
|
const provider = options.provider || 'local';
|
|
609
|
-
return `/**
|
|
610
|
-
* Qate Reporter
|
|
611
|
-
* Reports test results back to Qate for tracking and analytics
|
|
612
|
-
*
|
|
613
|
-
* Generated by Qate CLI
|
|
614
|
-
*/
|
|
615
|
-
|
|
616
|
-
import
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
private
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
623
|
+
return `/**
|
|
624
|
+
* Qate Reporter
|
|
625
|
+
* Reports test results back to Qate for tracking and analytics
|
|
626
|
+
*
|
|
627
|
+
* Generated by Qate CLI
|
|
628
|
+
*/
|
|
629
|
+
|
|
630
|
+
import fs from 'fs';
|
|
631
|
+
import type {
|
|
632
|
+
Reporter,
|
|
633
|
+
FullConfig,
|
|
634
|
+
Suite,
|
|
635
|
+
TestCase,
|
|
636
|
+
TestResult,
|
|
637
|
+
FullResult,
|
|
638
|
+
} from '@playwright/test/reporter';
|
|
639
|
+
import type { StepTiming } from './qate-runtime';
|
|
640
|
+
|
|
641
|
+
// Qate configuration (hardcoded at generation time)
|
|
642
|
+
const EXECUTION_ID = '${executionId}';
|
|
643
|
+
const API_URL = '${apiUrl}';
|
|
644
|
+
const PROVIDER = '${provider}';
|
|
645
|
+
|
|
646
|
+
// Test ID mapping from Qate
|
|
647
|
+
const TEST_MAPPING: { [filename: string]: { testId: string; testName: string } } = ${testMappingJson};
|
|
648
|
+
|
|
649
|
+
interface StepNetworkRequest {
|
|
650
|
+
url: string;
|
|
651
|
+
method: string;
|
|
652
|
+
status?: number;
|
|
653
|
+
contentType?: string;
|
|
654
|
+
duration?: number;
|
|
655
|
+
failure?: string;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
interface StepResult {
|
|
659
|
+
stepId?: string;
|
|
660
|
+
stepIndex: number;
|
|
661
|
+
description: string;
|
|
662
|
+
status: 'passed' | 'failed' | 'error' | 'skipped';
|
|
663
|
+
duration: number;
|
|
664
|
+
error?: string;
|
|
665
|
+
performanceData?: {
|
|
666
|
+
activityStart: string;
|
|
667
|
+
activityEnd: string;
|
|
668
|
+
activityDuration: number;
|
|
669
|
+
activityDurationFormatted: string;
|
|
670
|
+
settledEnd?: string;
|
|
671
|
+
settledDuration?: number;
|
|
672
|
+
settledDurationFormatted?: string;
|
|
673
|
+
};
|
|
674
|
+
networkRequests?: StepNetworkRequest[];
|
|
675
|
+
screenshot?: string;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
interface TestResultData {
|
|
679
|
+
testId: string;
|
|
680
|
+
testName: string;
|
|
681
|
+
status: 'passed' | 'failed' | 'error';
|
|
682
|
+
duration: number;
|
|
683
|
+
startedAt: string;
|
|
684
|
+
steps: StepResult[];
|
|
685
|
+
error?: string;
|
|
686
|
+
metadata?: Record<string, unknown>;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
class QateReporter implements Reporter {
|
|
690
|
+
private results: TestResultData[] = [];
|
|
691
|
+
private startTime: Date = new Date();
|
|
692
|
+
private config: FullConfig | null = null;
|
|
693
|
+
|
|
694
|
+
onBegin(config: FullConfig, suite: Suite) {
|
|
695
|
+
this.config = config;
|
|
696
|
+
this.startTime = new Date();
|
|
697
|
+
console.log('\\n[Qate] Starting test execution...');
|
|
698
|
+
console.log('[Qate] Execution ID:', EXECUTION_ID || 'not set');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
onTestEnd(test: TestCase, result: TestResult) {
|
|
702
|
+
// Find test mapping by filename
|
|
703
|
+
const filePath = test.location.file;
|
|
704
|
+
const filename = filePath.split('/').pop()?.split('\\\\').pop() || '';
|
|
705
|
+
const mapping = TEST_MAPPING[filename];
|
|
706
|
+
|
|
707
|
+
if (!mapping) {
|
|
708
|
+
console.warn('[Qate] No mapping found for test file:', filename);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Get step timings from test attachments (cross-process communication from worker)
|
|
713
|
+
let collectedStepTimings: StepTiming[] = [];
|
|
714
|
+
const stepTimingsAttachment = result.attachments.find(a => a.name === 'qate-step-timings');
|
|
715
|
+
if (stepTimingsAttachment?.body) {
|
|
716
|
+
try {
|
|
717
|
+
collectedStepTimings = JSON.parse(stepTimingsAttachment.body.toString());
|
|
718
|
+
} catch (e) {
|
|
719
|
+
console.warn('[Qate] Failed to parse step timings attachment');
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Helper to format duration
|
|
724
|
+
const formatDuration = (ms: number): string => {
|
|
725
|
+
if (ms < 1000) return \`\${Math.round(ms)}ms\`;
|
|
726
|
+
if (ms < 60000) return \`\${(ms / 1000).toFixed(2)}s\`;
|
|
727
|
+
const minutes = Math.floor(ms / 60000);
|
|
728
|
+
const seconds = ((ms % 60000) / 1000).toFixed(1);
|
|
729
|
+
return \`\${minutes}m \${seconds}s\`;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Build steps from stepTimings (collected via qateRecordStepTiming calls in generated test code)
|
|
733
|
+
// This is more reliable than result.steps which only contains steps defined via test.step()
|
|
734
|
+
const steps: StepResult[] = collectedStepTimings
|
|
735
|
+
.sort((a, b) => a.stepIndex - b.stepIndex)
|
|
736
|
+
.map((timing) => {
|
|
737
|
+
const stepResult: StepResult = {
|
|
738
|
+
stepId: timing.stepId,
|
|
739
|
+
stepIndex: timing.stepIndex,
|
|
740
|
+
description: timing.description,
|
|
741
|
+
status: timing.status,
|
|
742
|
+
duration: timing.settledDuration,
|
|
743
|
+
error: timing.error,
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// Add performance data
|
|
747
|
+
stepResult.performanceData = {
|
|
748
|
+
activityStart: new Date(timing.activityStart).toISOString(),
|
|
749
|
+
activityEnd: new Date(timing.activityEnd).toISOString(),
|
|
750
|
+
activityDuration: timing.activityDuration,
|
|
751
|
+
activityDurationFormatted: formatDuration(timing.activityDuration),
|
|
752
|
+
settledEnd: new Date(timing.settledEnd).toISOString(),
|
|
753
|
+
settledDuration: timing.settledDuration,
|
|
754
|
+
settledDurationFormatted: formatDuration(timing.settledDuration),
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Add network requests if captured
|
|
758
|
+
if (timing.networkRequests && timing.networkRequests.length > 0) {
|
|
759
|
+
stepResult.networkRequests = timing.networkRequests;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return stepResult;
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Log step count for debugging
|
|
766
|
+
console.log(\`[Qate] Collected \${steps.length} step results from attachment\`);
|
|
767
|
+
|
|
768
|
+
// Read Playwright screenshot attachment on failure and attach to last failed step
|
|
769
|
+
if (result.status !== 'passed') {
|
|
770
|
+
const screenshotAttachment = result.attachments.find(
|
|
771
|
+
(a: { name: string; contentType: string; path?: string }) =>
|
|
772
|
+
a.name === 'screenshot' && a.contentType === 'image/png' && a.path
|
|
773
|
+
);
|
|
774
|
+
if (screenshotAttachment?.path) {
|
|
775
|
+
try {
|
|
776
|
+
const buffer = fs.readFileSync(screenshotAttachment.path);
|
|
777
|
+
const screenshotBase64 = buffer.toString('base64');
|
|
778
|
+
// Attach to the last failed/error step
|
|
779
|
+
const lastFailedStep = [...steps].reverse().find(s => s.status === 'failed' || s.status === 'error');
|
|
780
|
+
if (lastFailedStep) {
|
|
781
|
+
lastFailedStep.screenshot = screenshotBase64;
|
|
782
|
+
}
|
|
783
|
+
} catch { /* non-fatal */ }
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const testResult: TestResultData = {
|
|
788
|
+
testId: mapping.testId,
|
|
789
|
+
testName: mapping.testName,
|
|
790
|
+
status: result.status === 'passed' ? 'passed' : result.status === 'timedOut' ? 'error' : 'failed',
|
|
791
|
+
duration: result.duration,
|
|
792
|
+
startedAt: result.startTime.toISOString(),
|
|
793
|
+
steps,
|
|
794
|
+
error: result.error?.message,
|
|
795
|
+
metadata: {
|
|
796
|
+
retries: result.retry,
|
|
797
|
+
workerIndex: result.workerIndex,
|
|
798
|
+
attachments: result.attachments.map(a => ({ name: a.name, contentType: a.contentType })),
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
this.results.push(testResult);
|
|
803
|
+
|
|
804
|
+
const statusIcon = testResult.status === 'passed' ? '✓' : '✗';
|
|
805
|
+
console.log(\`[Qate] \${statusIcon} \${mapping.testName} (\${testResult.duration}ms)\`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async onEnd(result: FullResult) {
|
|
809
|
+
const apiKey = process.env.QATE_API_KEY;
|
|
810
|
+
|
|
811
|
+
if (!EXECUTION_ID) {
|
|
812
|
+
console.log('\\n[Qate] No execution ID configured - skipping result reporting');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!apiKey) {
|
|
817
|
+
console.log('\\n[Qate] QATE_API_KEY not set - skipping result reporting');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
console.log('\\n[Qate] Reporting results to Qate...');
|
|
822
|
+
|
|
823
|
+
const payload = {
|
|
824
|
+
executionId: EXECUTION_ID,
|
|
825
|
+
status: result.status,
|
|
826
|
+
results: this.results,
|
|
827
|
+
summary: {
|
|
828
|
+
total: this.results.length,
|
|
829
|
+
passed: this.results.filter(r => r.status === 'passed').length,
|
|
830
|
+
failed: this.results.filter(r => r.status === 'failed').length,
|
|
831
|
+
error: this.results.filter(r => r.status === 'error').length,
|
|
832
|
+
status: result.status,
|
|
833
|
+
},
|
|
834
|
+
startedAt: this.startTime.toISOString(),
|
|
835
|
+
completedAt: new Date().toISOString(),
|
|
836
|
+
provider: PROVIDER,
|
|
837
|
+
environment: {
|
|
838
|
+
browser: this.config?.projects?.[0]?.name || 'unknown',
|
|
839
|
+
os: process.platform,
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const controller = new AbortController();
|
|
845
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
846
|
+
|
|
847
|
+
const response = await fetch(\`\${API_URL}/ci/report\`, {
|
|
848
|
+
method: 'POST',
|
|
849
|
+
headers: {
|
|
850
|
+
'Content-Type': 'application/json',
|
|
851
|
+
'X-API-Key': apiKey,
|
|
852
|
+
},
|
|
853
|
+
body: JSON.stringify(payload),
|
|
854
|
+
signal: controller.signal,
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
clearTimeout(timeoutId);
|
|
858
|
+
|
|
859
|
+
if (response.ok) {
|
|
860
|
+
const data = await response.json();
|
|
861
|
+
console.log('[Qate] Results reported successfully');
|
|
862
|
+
console.log(\`[Qate] Summary: \${data.summary?.passed || 0} passed, \${data.summary?.failed || 0} failed\`);
|
|
863
|
+
console.log(\`[Qate] View results at: https://app.qate.ai/executions/\${EXECUTION_ID}\`);
|
|
864
|
+
} else {
|
|
865
|
+
const error = await response.text();
|
|
866
|
+
console.error('[Qate] Failed to report results:', response.status, error);
|
|
867
|
+
}
|
|
868
|
+
} catch (error: any) {
|
|
869
|
+
if (error.name === 'AbortError') {
|
|
870
|
+
console.error('[Qate] Request timed out after 30 seconds');
|
|
871
|
+
} else {
|
|
872
|
+
console.error('[Qate] Error reporting results:', error.message || error);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Small delay to ensure HTTP connection is properly closed before Playwright cleanup
|
|
877
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export default QateReporter;
|
|
847
882
|
`;
|
|
848
883
|
}
|
|
849
884
|
/**
|
|
@@ -851,265 +886,265 @@ export default QateReporter;
|
|
|
851
886
|
*/
|
|
852
887
|
function generateQateRuntime(apiUrl, executionId) {
|
|
853
888
|
const trackingEnabled = executionId ? 'true' : 'false';
|
|
854
|
-
return `/**
|
|
855
|
-
* Qate Runtime Helper
|
|
856
|
-
* Resolves placeholders at runtime by calling the Qate API
|
|
857
|
-
*
|
|
858
|
-
* Generated by Qate CLI
|
|
859
|
-
*/
|
|
860
|
-
|
|
861
|
-
export interface QateTestContext {
|
|
862
|
-
testId: string;
|
|
863
|
-
testName: string;
|
|
864
|
-
datasetId?: string | null;
|
|
865
|
-
authRoleId?: string | null;
|
|
866
|
-
placeholders: string[];
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
interface PlaceholderResolutionResult {
|
|
870
|
-
success: boolean;
|
|
871
|
-
values: Record<string, string>;
|
|
872
|
-
metadata: {
|
|
873
|
-
totpTimeRemaining?: number | null;
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// Cache for non-TOTP values (cleared per test)
|
|
878
|
-
let placeholderCache: Record<string, string> = {};
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Resolve placeholders by calling the Qate API
|
|
882
|
-
* Note: TOTP values are always fetched fresh (not cached)
|
|
883
|
-
*/
|
|
884
|
-
export async function qateResolve(context: QateTestContext): Promise<Record<string, string>> {
|
|
885
|
-
const apiKey = process.env.QATE_API_KEY;
|
|
886
|
-
const apiUrl = '${apiUrl}';
|
|
887
|
-
|
|
888
|
-
if (!apiKey) {
|
|
889
|
-
console.warn('[Qate Runtime] QATE_API_KEY not set - placeholders will not be resolved');
|
|
890
|
-
return {};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
if (context.placeholders.length === 0) {
|
|
894
|
-
return {};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Clear cache for new test
|
|
898
|
-
placeholderCache = {};
|
|
899
|
-
|
|
900
|
-
try {
|
|
901
|
-
const response = await fetch(\`\${apiUrl}/ci/resolve-placeholders\`, {
|
|
902
|
-
method: 'POST',
|
|
903
|
-
headers: {
|
|
904
|
-
'Content-Type': 'application/json',
|
|
905
|
-
'X-API-Key': apiKey,
|
|
906
|
-
},
|
|
907
|
-
body: JSON.stringify({
|
|
908
|
-
testId: context.testId,
|
|
909
|
-
datasetId: context.datasetId,
|
|
910
|
-
authRoleId: context.authRoleId,
|
|
911
|
-
placeholders: context.placeholders,
|
|
912
|
-
}),
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
if (!response.ok) {
|
|
916
|
-
const error = await response.text();
|
|
917
|
-
console.error('[Qate Runtime] Failed to resolve placeholders:', response.status, error);
|
|
918
|
-
return {};
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const data: PlaceholderResolutionResult = await response.json();
|
|
922
|
-
|
|
923
|
-
if (!data.success) {
|
|
924
|
-
console.warn('[Qate Runtime] Placeholder resolution returned unsuccessful');
|
|
925
|
-
return {};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// Cache non-TOTP values
|
|
929
|
-
for (const [key, value] of Object.entries(data.values)) {
|
|
930
|
-
if (key !== 'mfa_code') {
|
|
931
|
-
placeholderCache[key] = value;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Log TOTP time remaining if present
|
|
936
|
-
if (data.metadata?.totpTimeRemaining != null) {
|
|
937
|
-
console.log(\`[Qate Runtime] TOTP time remaining: \${data.metadata.totpTimeRemaining}s\`);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
return data.values;
|
|
941
|
-
} catch (error) {
|
|
942
|
-
console.error('[Qate Runtime] Error resolving placeholders:', error);
|
|
943
|
-
return {};
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* Replace placeholder patterns in a string with resolved values
|
|
949
|
-
*/
|
|
950
|
-
export function replacePlaceholders(text: string, values: Record<string, string>): string {
|
|
951
|
-
return text.replace(/\{\{([^}]+)\}\}/g, (match, name) => {
|
|
952
|
-
if (name in values) {
|
|
953
|
-
return values[name];
|
|
954
|
-
}
|
|
955
|
-
console.warn(\`[Qate Runtime] Unresolved placeholder: \${name}\`);
|
|
956
|
-
return match; // Return original if not resolved
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Step timing storage - collected by QateReporter after test completes
|
|
961
|
-
export interface StepTiming {
|
|
962
|
-
stepId: string;
|
|
963
|
-
stepIndex: number;
|
|
964
|
-
description: string;
|
|
965
|
-
status: 'passed' | 'failed';
|
|
966
|
-
error?: string;
|
|
967
|
-
activityStart: number;
|
|
968
|
-
activityEnd: number;
|
|
969
|
-
settledEnd: number;
|
|
970
|
-
activityDuration: number;
|
|
971
|
-
settledDuration: number;
|
|
972
|
-
networkRequests?: NetworkRequest[];
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Global storage for step timings (cleared per test)
|
|
976
|
-
export const stepTimings: StepTiming[] = [];
|
|
977
|
-
|
|
978
|
-
// Network activity capture (only when tracking is enabled)
|
|
979
|
-
export interface NetworkRequest {
|
|
980
|
-
url: string;
|
|
981
|
-
method: string;
|
|
982
|
-
status?: number;
|
|
983
|
-
contentType?: string;
|
|
984
|
-
duration?: number;
|
|
985
|
-
failure?: string;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Per-step network capture state
|
|
989
|
-
let currentStepNetworkRequests: NetworkRequest[] = [];
|
|
990
|
-
let pendingRequests = new Map<string, { url: string; method: string; requestTime: number }>();
|
|
991
|
-
let networkCaptureEnabled = false;
|
|
992
|
-
const MAX_NETWORK_REQUESTS_PER_STEP = 20;
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Set up network capture on a Playwright page
|
|
996
|
-
* Only captures when QATE_EXECUTION_ID is set (tracking enabled)
|
|
997
|
-
*/
|
|
998
|
-
export function qateSetupNetworkCapture(page: any): void {
|
|
999
|
-
// Skip if tracking is disabled
|
|
1000
|
-
if (!${trackingEnabled}) {
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
networkCaptureEnabled = true;
|
|
1004
|
-
|
|
1005
|
-
page.on('request', (request: any) => {
|
|
1006
|
-
if (!networkCaptureEnabled) return;
|
|
1007
|
-
if (currentStepNetworkRequests.length >= MAX_NETWORK_REQUESTS_PER_STEP) return;
|
|
1008
|
-
|
|
1009
|
-
const url = request.url();
|
|
1010
|
-
// Skip data URLs, chrome-extension, and static assets
|
|
1011
|
-
if (url.startsWith('data:') || url.startsWith('chrome-extension:') ||
|
|
1012
|
-
url.match(/\\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico|css)$/i)) {
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
pendingRequests.set(url, {
|
|
1017
|
-
url: url.length > 500 ? url.substring(0, 500) + '...' : url,
|
|
1018
|
-
method: request.method(),
|
|
1019
|
-
requestTime: Date.now(),
|
|
1020
|
-
});
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
page.on('response', (response: any) => {
|
|
1024
|
-
if (!networkCaptureEnabled) return;
|
|
1025
|
-
const url = response.url();
|
|
1026
|
-
const pending = pendingRequests.get(url);
|
|
1027
|
-
if (pending && currentStepNetworkRequests.length < MAX_NETWORK_REQUESTS_PER_STEP) {
|
|
1028
|
-
currentStepNetworkRequests.push({
|
|
1029
|
-
url: pending.url,
|
|
1030
|
-
method: pending.method,
|
|
1031
|
-
status: response.status(),
|
|
1032
|
-
contentType: response.headers()['content-type']?.split(';')[0],
|
|
1033
|
-
duration: Date.now() - pending.requestTime,
|
|
1034
|
-
});
|
|
1035
|
-
pendingRequests.delete(url);
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
page.on('requestfailed', (request: any) => {
|
|
1040
|
-
if (!networkCaptureEnabled) return;
|
|
1041
|
-
const url = request.url();
|
|
1042
|
-
const pending = pendingRequests.get(url);
|
|
1043
|
-
if (pending && currentStepNetworkRequests.length < MAX_NETWORK_REQUESTS_PER_STEP) {
|
|
1044
|
-
currentStepNetworkRequests.push({
|
|
1045
|
-
url: pending.url,
|
|
1046
|
-
method: pending.method,
|
|
1047
|
-
failure: request.failure()?.errorText || 'Request failed',
|
|
1048
|
-
duration: Date.now() - pending.requestTime,
|
|
1049
|
-
});
|
|
1050
|
-
pendingRequests.delete(url);
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
* Start capturing network for a new step (clears previous step's data)
|
|
1057
|
-
*/
|
|
1058
|
-
export function qateStartStepNetworkCapture(): void {
|
|
1059
|
-
currentStepNetworkRequests = [];
|
|
1060
|
-
pendingRequests.clear();
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Get network requests captured during the current step
|
|
1065
|
-
*/
|
|
1066
|
-
export function qateGetStepNetworkRequests(): NetworkRequest[] {
|
|
1067
|
-
return [...currentStepNetworkRequests];
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Record step timing data for performance measurement
|
|
1072
|
-
* Called by generated test code after each action
|
|
1073
|
-
*/
|
|
1074
|
-
export function qateRecordStepTiming(
|
|
1075
|
-
stepId: string,
|
|
1076
|
-
stepIndex: number,
|
|
1077
|
-
description: string,
|
|
1078
|
-
status: 'passed' | 'failed',
|
|
1079
|
-
activityStart: number,
|
|
1080
|
-
activityEnd: number,
|
|
1081
|
-
settledEnd: number,
|
|
1082
|
-
error?: string
|
|
1083
|
-
): void {
|
|
1084
|
-
const activityDuration = activityEnd - activityStart;
|
|
1085
|
-
const settledDuration = settledEnd - activityStart;
|
|
1086
|
-
|
|
1087
|
-
// Capture network requests for this step (if tracking enabled)
|
|
1088
|
-
const networkReqs = qateGetStepNetworkRequests();
|
|
1089
|
-
|
|
1090
|
-
stepTimings.push({
|
|
1091
|
-
stepId,
|
|
1092
|
-
stepIndex,
|
|
1093
|
-
description,
|
|
1094
|
-
status,
|
|
1095
|
-
error,
|
|
1096
|
-
activityStart,
|
|
1097
|
-
activityEnd,
|
|
1098
|
-
settledEnd,
|
|
1099
|
-
activityDuration,
|
|
1100
|
-
settledDuration,
|
|
1101
|
-
networkRequests: networkReqs.length > 0 ? networkReqs : undefined,
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
/**
|
|
1106
|
-
* Get and clear step timings (called by reporter)
|
|
1107
|
-
*/
|
|
1108
|
-
export function qateGetAndClearStepTimings(): StepTiming[] {
|
|
1109
|
-
const timings = [...stepTimings];
|
|
1110
|
-
stepTimings.length = 0;
|
|
1111
|
-
return timings;
|
|
1112
|
-
}
|
|
889
|
+
return `/**
|
|
890
|
+
* Qate Runtime Helper
|
|
891
|
+
* Resolves placeholders at runtime by calling the Qate API
|
|
892
|
+
*
|
|
893
|
+
* Generated by Qate CLI
|
|
894
|
+
*/
|
|
895
|
+
|
|
896
|
+
export interface QateTestContext {
|
|
897
|
+
testId: string;
|
|
898
|
+
testName: string;
|
|
899
|
+
datasetId?: string | null;
|
|
900
|
+
authRoleId?: string | null;
|
|
901
|
+
placeholders: string[];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
interface PlaceholderResolutionResult {
|
|
905
|
+
success: boolean;
|
|
906
|
+
values: Record<string, string>;
|
|
907
|
+
metadata: {
|
|
908
|
+
totpTimeRemaining?: number | null;
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Cache for non-TOTP values (cleared per test)
|
|
913
|
+
let placeholderCache: Record<string, string> = {};
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Resolve placeholders by calling the Qate API
|
|
917
|
+
* Note: TOTP values are always fetched fresh (not cached)
|
|
918
|
+
*/
|
|
919
|
+
export async function qateResolve(context: QateTestContext): Promise<Record<string, string>> {
|
|
920
|
+
const apiKey = process.env.QATE_API_KEY;
|
|
921
|
+
const apiUrl = '${apiUrl}';
|
|
922
|
+
|
|
923
|
+
if (!apiKey) {
|
|
924
|
+
console.warn('[Qate Runtime] QATE_API_KEY not set - placeholders will not be resolved');
|
|
925
|
+
return {};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (context.placeholders.length === 0) {
|
|
929
|
+
return {};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Clear cache for new test
|
|
933
|
+
placeholderCache = {};
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
const response = await fetch(\`\${apiUrl}/ci/resolve-placeholders\`, {
|
|
937
|
+
method: 'POST',
|
|
938
|
+
headers: {
|
|
939
|
+
'Content-Type': 'application/json',
|
|
940
|
+
'X-API-Key': apiKey,
|
|
941
|
+
},
|
|
942
|
+
body: JSON.stringify({
|
|
943
|
+
testId: context.testId,
|
|
944
|
+
datasetId: context.datasetId,
|
|
945
|
+
authRoleId: context.authRoleId,
|
|
946
|
+
placeholders: context.placeholders,
|
|
947
|
+
}),
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
if (!response.ok) {
|
|
951
|
+
const error = await response.text();
|
|
952
|
+
console.error('[Qate Runtime] Failed to resolve placeholders:', response.status, error);
|
|
953
|
+
return {};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const data: PlaceholderResolutionResult = await response.json();
|
|
957
|
+
|
|
958
|
+
if (!data.success) {
|
|
959
|
+
console.warn('[Qate Runtime] Placeholder resolution returned unsuccessful');
|
|
960
|
+
return {};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Cache non-TOTP values
|
|
964
|
+
for (const [key, value] of Object.entries(data.values)) {
|
|
965
|
+
if (key !== 'mfa_code') {
|
|
966
|
+
placeholderCache[key] = value;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Log TOTP time remaining if present
|
|
971
|
+
if (data.metadata?.totpTimeRemaining != null) {
|
|
972
|
+
console.log(\`[Qate Runtime] TOTP time remaining: \${data.metadata.totpTimeRemaining}s\`);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return data.values;
|
|
976
|
+
} catch (error) {
|
|
977
|
+
console.error('[Qate Runtime] Error resolving placeholders:', error);
|
|
978
|
+
return {};
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Replace placeholder patterns in a string with resolved values
|
|
984
|
+
*/
|
|
985
|
+
export function replacePlaceholders(text: string, values: Record<string, string>): string {
|
|
986
|
+
return text.replace(/\{\{([^}]+)\}\}/g, (match, name) => {
|
|
987
|
+
if (name in values) {
|
|
988
|
+
return values[name];
|
|
989
|
+
}
|
|
990
|
+
console.warn(\`[Qate Runtime] Unresolved placeholder: \${name}\`);
|
|
991
|
+
return match; // Return original if not resolved
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Step timing storage - collected by QateReporter after test completes
|
|
996
|
+
export interface StepTiming {
|
|
997
|
+
stepId: string;
|
|
998
|
+
stepIndex: number;
|
|
999
|
+
description: string;
|
|
1000
|
+
status: 'passed' | 'failed';
|
|
1001
|
+
error?: string;
|
|
1002
|
+
activityStart: number;
|
|
1003
|
+
activityEnd: number;
|
|
1004
|
+
settledEnd: number;
|
|
1005
|
+
activityDuration: number;
|
|
1006
|
+
settledDuration: number;
|
|
1007
|
+
networkRequests?: NetworkRequest[];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Global storage for step timings (cleared per test)
|
|
1011
|
+
export const stepTimings: StepTiming[] = [];
|
|
1012
|
+
|
|
1013
|
+
// Network activity capture (only when tracking is enabled)
|
|
1014
|
+
export interface NetworkRequest {
|
|
1015
|
+
url: string;
|
|
1016
|
+
method: string;
|
|
1017
|
+
status?: number;
|
|
1018
|
+
contentType?: string;
|
|
1019
|
+
duration?: number;
|
|
1020
|
+
failure?: string;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Per-step network capture state
|
|
1024
|
+
let currentStepNetworkRequests: NetworkRequest[] = [];
|
|
1025
|
+
let pendingRequests = new Map<string, { url: string; method: string; requestTime: number }>();
|
|
1026
|
+
let networkCaptureEnabled = false;
|
|
1027
|
+
const MAX_NETWORK_REQUESTS_PER_STEP = 20;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Set up network capture on a Playwright page
|
|
1031
|
+
* Only captures when QATE_EXECUTION_ID is set (tracking enabled)
|
|
1032
|
+
*/
|
|
1033
|
+
export function qateSetupNetworkCapture(page: any): void {
|
|
1034
|
+
// Skip if tracking is disabled
|
|
1035
|
+
if (!${trackingEnabled}) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
networkCaptureEnabled = true;
|
|
1039
|
+
|
|
1040
|
+
page.on('request', (request: any) => {
|
|
1041
|
+
if (!networkCaptureEnabled) return;
|
|
1042
|
+
if (currentStepNetworkRequests.length >= MAX_NETWORK_REQUESTS_PER_STEP) return;
|
|
1043
|
+
|
|
1044
|
+
const url = request.url();
|
|
1045
|
+
// Skip data URLs, chrome-extension, and static assets
|
|
1046
|
+
if (url.startsWith('data:') || url.startsWith('chrome-extension:') ||
|
|
1047
|
+
url.match(/\\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico|css)$/i)) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
pendingRequests.set(url, {
|
|
1052
|
+
url: url.length > 500 ? url.substring(0, 500) + '...' : url,
|
|
1053
|
+
method: request.method(),
|
|
1054
|
+
requestTime: Date.now(),
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
page.on('response', (response: any) => {
|
|
1059
|
+
if (!networkCaptureEnabled) return;
|
|
1060
|
+
const url = response.url();
|
|
1061
|
+
const pending = pendingRequests.get(url);
|
|
1062
|
+
if (pending && currentStepNetworkRequests.length < MAX_NETWORK_REQUESTS_PER_STEP) {
|
|
1063
|
+
currentStepNetworkRequests.push({
|
|
1064
|
+
url: pending.url,
|
|
1065
|
+
method: pending.method,
|
|
1066
|
+
status: response.status(),
|
|
1067
|
+
contentType: response.headers()['content-type']?.split(';')[0],
|
|
1068
|
+
duration: Date.now() - pending.requestTime,
|
|
1069
|
+
});
|
|
1070
|
+
pendingRequests.delete(url);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
page.on('requestfailed', (request: any) => {
|
|
1075
|
+
if (!networkCaptureEnabled) return;
|
|
1076
|
+
const url = request.url();
|
|
1077
|
+
const pending = pendingRequests.get(url);
|
|
1078
|
+
if (pending && currentStepNetworkRequests.length < MAX_NETWORK_REQUESTS_PER_STEP) {
|
|
1079
|
+
currentStepNetworkRequests.push({
|
|
1080
|
+
url: pending.url,
|
|
1081
|
+
method: pending.method,
|
|
1082
|
+
failure: request.failure()?.errorText || 'Request failed',
|
|
1083
|
+
duration: Date.now() - pending.requestTime,
|
|
1084
|
+
});
|
|
1085
|
+
pendingRequests.delete(url);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Start capturing network for a new step (clears previous step's data)
|
|
1092
|
+
*/
|
|
1093
|
+
export function qateStartStepNetworkCapture(): void {
|
|
1094
|
+
currentStepNetworkRequests = [];
|
|
1095
|
+
pendingRequests.clear();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Get network requests captured during the current step
|
|
1100
|
+
*/
|
|
1101
|
+
export function qateGetStepNetworkRequests(): NetworkRequest[] {
|
|
1102
|
+
return [...currentStepNetworkRequests];
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Record step timing data for performance measurement
|
|
1107
|
+
* Called by generated test code after each action
|
|
1108
|
+
*/
|
|
1109
|
+
export function qateRecordStepTiming(
|
|
1110
|
+
stepId: string,
|
|
1111
|
+
stepIndex: number,
|
|
1112
|
+
description: string,
|
|
1113
|
+
status: 'passed' | 'failed',
|
|
1114
|
+
activityStart: number,
|
|
1115
|
+
activityEnd: number,
|
|
1116
|
+
settledEnd: number,
|
|
1117
|
+
error?: string
|
|
1118
|
+
): void {
|
|
1119
|
+
const activityDuration = activityEnd - activityStart;
|
|
1120
|
+
const settledDuration = settledEnd - activityStart;
|
|
1121
|
+
|
|
1122
|
+
// Capture network requests for this step (if tracking enabled)
|
|
1123
|
+
const networkReqs = qateGetStepNetworkRequests();
|
|
1124
|
+
|
|
1125
|
+
stepTimings.push({
|
|
1126
|
+
stepId,
|
|
1127
|
+
stepIndex,
|
|
1128
|
+
description,
|
|
1129
|
+
status,
|
|
1130
|
+
error,
|
|
1131
|
+
activityStart,
|
|
1132
|
+
activityEnd,
|
|
1133
|
+
settledEnd,
|
|
1134
|
+
activityDuration,
|
|
1135
|
+
settledDuration,
|
|
1136
|
+
networkRequests: networkReqs.length > 0 ? networkReqs : undefined,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Get and clear step timings (called by reporter)
|
|
1142
|
+
*/
|
|
1143
|
+
export function qateGetAndClearStepTimings(): StepTiming[] {
|
|
1144
|
+
const timings = [...stepTimings];
|
|
1145
|
+
stepTimings.length = 0;
|
|
1146
|
+
return timings;
|
|
1147
|
+
}
|
|
1113
1148
|
`;
|
|
1114
1149
|
}
|
|
1115
1150
|
/**
|
|
@@ -1149,37 +1184,37 @@ function generateSauceConfig(appUrl, options) {
|
|
|
1149
1184
|
const slBrowser = toSauceLabsBrowser(browser);
|
|
1150
1185
|
const slPlatform = toSauceLabsPlatform(os, osVersion);
|
|
1151
1186
|
const browserDisplayName = browser.charAt(0).toUpperCase() + browser.slice(1).toLowerCase();
|
|
1152
|
-
return `# Sauce Labs Configuration
|
|
1153
|
-
# Docs: https://docs.saucelabs.com/web-apps/automated-testing/playwright/yaml/
|
|
1154
|
-
apiVersion: v1alpha
|
|
1155
|
-
kind: playwright
|
|
1156
|
-
defaults:
|
|
1157
|
-
timeout: 30m
|
|
1158
|
-
sauce:
|
|
1159
|
-
region: us-west-1
|
|
1160
|
-
concurrency: 5
|
|
1161
|
-
metadata:
|
|
1162
|
-
tags:
|
|
1163
|
-
- qate
|
|
1164
|
-
- e2e
|
|
1165
|
-
build: "\${BUILD_NUMBER:-local}"
|
|
1166
|
-
playwright:
|
|
1167
|
-
version: package.json
|
|
1168
|
-
rootDir: ./
|
|
1169
|
-
suites:
|
|
1170
|
-
- name: "Qate E2E Tests - ${browserDisplayName}"
|
|
1171
|
-
platformName: "${slPlatform}"
|
|
1172
|
-
testMatch: ["tests/*.spec.ts"]
|
|
1173
|
-
params:
|
|
1174
|
-
browserName: "${slBrowser}"
|
|
1175
|
-
baseURL: "${appUrl}"
|
|
1176
|
-
artifacts:
|
|
1177
|
-
download:
|
|
1178
|
-
when: always
|
|
1179
|
-
match:
|
|
1180
|
-
- "playwright-report/*"
|
|
1181
|
-
- "test-results/*"
|
|
1182
|
-
directory: ./artifacts
|
|
1187
|
+
return `# Sauce Labs Configuration
|
|
1188
|
+
# Docs: https://docs.saucelabs.com/web-apps/automated-testing/playwright/yaml/
|
|
1189
|
+
apiVersion: v1alpha
|
|
1190
|
+
kind: playwright
|
|
1191
|
+
defaults:
|
|
1192
|
+
timeout: 30m
|
|
1193
|
+
sauce:
|
|
1194
|
+
region: us-west-1
|
|
1195
|
+
concurrency: 5
|
|
1196
|
+
metadata:
|
|
1197
|
+
tags:
|
|
1198
|
+
- qate
|
|
1199
|
+
- e2e
|
|
1200
|
+
build: "\${BUILD_NUMBER:-local}"
|
|
1201
|
+
playwright:
|
|
1202
|
+
version: package.json
|
|
1203
|
+
rootDir: ./
|
|
1204
|
+
suites:
|
|
1205
|
+
- name: "Qate E2E Tests - ${browserDisplayName}"
|
|
1206
|
+
platformName: "${slPlatform}"
|
|
1207
|
+
testMatch: ["tests/*.spec.ts"]
|
|
1208
|
+
params:
|
|
1209
|
+
browserName: "${slBrowser}"
|
|
1210
|
+
baseURL: "${appUrl}"
|
|
1211
|
+
artifacts:
|
|
1212
|
+
download:
|
|
1213
|
+
when: always
|
|
1214
|
+
match:
|
|
1215
|
+
- "playwright-report/*"
|
|
1216
|
+
- "test-results/*"
|
|
1217
|
+
directory: ./artifacts
|
|
1183
1218
|
`;
|
|
1184
1219
|
}
|
|
1185
1220
|
/**
|
|
@@ -1189,31 +1224,31 @@ function generateBrowserStackConfig(options) {
|
|
|
1189
1224
|
const { browser = 'chrome', browserVersion = 'latest', os = 'windows', osVersion = '11' } = options;
|
|
1190
1225
|
const bsBrowser = toBrowserStackBrowser(browser);
|
|
1191
1226
|
const bsOS = toBrowserStackOS(os);
|
|
1192
|
-
return `# BrowserStack Configuration
|
|
1193
|
-
# Docs: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs
|
|
1194
|
-
#
|
|
1195
|
-
# Run with: npx browserstack-node-sdk playwright test
|
|
1196
|
-
|
|
1197
|
-
userName: \${BROWSERSTACK_USERNAME}
|
|
1198
|
-
accessKey: \${BROWSERSTACK_ACCESS_KEY}
|
|
1199
|
-
|
|
1200
|
-
platforms:
|
|
1201
|
-
- os: ${bsOS}
|
|
1202
|
-
osVersion: "${osVersion}"
|
|
1203
|
-
browserName: ${bsBrowser}
|
|
1204
|
-
browserVersion: ${browserVersion}
|
|
1205
|
-
|
|
1206
|
-
parallelsPerPlatform: 5
|
|
1207
|
-
|
|
1208
|
-
browserstackLocal: false
|
|
1209
|
-
|
|
1210
|
-
projectName: "Qate Tests"
|
|
1211
|
-
buildName: "\${BUILD_NUMBER:-local}"
|
|
1212
|
-
buildIdentifier: "#\${BUILD_NUMBER:-1}"
|
|
1213
|
-
|
|
1214
|
-
debug: true
|
|
1215
|
-
networkLogs: true
|
|
1216
|
-
consoleLogs: verbose
|
|
1227
|
+
return `# BrowserStack Configuration
|
|
1228
|
+
# Docs: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs
|
|
1229
|
+
#
|
|
1230
|
+
# Run with: npx browserstack-node-sdk playwright test
|
|
1231
|
+
|
|
1232
|
+
userName: \${BROWSERSTACK_USERNAME}
|
|
1233
|
+
accessKey: \${BROWSERSTACK_ACCESS_KEY}
|
|
1234
|
+
|
|
1235
|
+
platforms:
|
|
1236
|
+
- os: ${bsOS}
|
|
1237
|
+
osVersion: "${osVersion}"
|
|
1238
|
+
browserName: ${bsBrowser}
|
|
1239
|
+
browserVersion: ${browserVersion}
|
|
1240
|
+
|
|
1241
|
+
parallelsPerPlatform: 5
|
|
1242
|
+
|
|
1243
|
+
browserstackLocal: false
|
|
1244
|
+
|
|
1245
|
+
projectName: "Qate Tests"
|
|
1246
|
+
buildName: "\${BUILD_NUMBER:-local}"
|
|
1247
|
+
buildIdentifier: "#\${BUILD_NUMBER:-1}"
|
|
1248
|
+
|
|
1249
|
+
debug: true
|
|
1250
|
+
networkLogs: true
|
|
1251
|
+
consoleLogs: verbose
|
|
1217
1252
|
`;
|
|
1218
1253
|
}
|
|
1219
1254
|
/**
|
|
@@ -1223,48 +1258,48 @@ function generateHyperExecuteConfig(options) {
|
|
|
1223
1258
|
const { browser = 'chrome', os = 'windows', osVersion = '11' } = options;
|
|
1224
1259
|
const ltBrowser = toLambdaTestBrowser(browser);
|
|
1225
1260
|
const runson = os.toLowerCase() === 'macos' || os.toLowerCase() === 'mac' ? 'mac' : 'linux';
|
|
1226
|
-
return `# LambdaTest HyperExecute Configuration
|
|
1227
|
-
# Docs: https://www.lambdatest.com/support/docs/hyperexecute-guided-walkthrough/
|
|
1228
|
-
#
|
|
1229
|
-
# Run with: ./hyperexecute --config hyperexecute.yaml
|
|
1230
|
-
# Download CLI: https://www.lambdatest.com/support/docs/hyperexecute-cli-run-tests-on-hyperexecute-grid/
|
|
1231
|
-
|
|
1232
|
-
version: "0.1"
|
|
1233
|
-
globalTimeout: 90
|
|
1234
|
-
testSuiteTimeout: 90
|
|
1235
|
-
testSuiteStep: 90
|
|
1236
|
-
|
|
1237
|
-
runson: ${runson}
|
|
1238
|
-
|
|
1239
|
-
autosplit: true
|
|
1240
|
-
retryOnFailure: false
|
|
1241
|
-
maxRetries: 1
|
|
1242
|
-
concurrency: 5
|
|
1243
|
-
|
|
1244
|
-
env:
|
|
1245
|
-
PLAYWRIGHT_BROWSERS_PATH: 0
|
|
1246
|
-
|
|
1247
|
-
pre:
|
|
1248
|
-
- npm install
|
|
1249
|
-
- npx playwright install ${ltBrowser === 'Chrome' ? 'chromium' : ltBrowser === 'pw-firefox' ? 'firefox' : 'webkit'}
|
|
1250
|
-
|
|
1251
|
-
testDiscovery:
|
|
1252
|
-
type: raw
|
|
1253
|
-
mode: dynamic
|
|
1254
|
-
command: grep -rn "test\\|it(" tests/*.spec.ts | cut -d':' -f1 | uniq
|
|
1255
|
-
|
|
1256
|
-
testRunnerCommand: npx playwright test $test --reporter=html
|
|
1257
|
-
|
|
1258
|
-
jobLabel:
|
|
1259
|
-
- qate
|
|
1260
|
-
- playwright
|
|
1261
|
-
- ${browser}
|
|
1262
|
-
|
|
1263
|
-
framework:
|
|
1264
|
-
name: playwright
|
|
1265
|
-
args:
|
|
1266
|
-
browserName: ${ltBrowser}
|
|
1267
|
-
platform: "${os === 'macos' || os === 'mac' ? 'MacOS' : 'Windows'} ${osVersion}"
|
|
1261
|
+
return `# LambdaTest HyperExecute Configuration
|
|
1262
|
+
# Docs: https://www.lambdatest.com/support/docs/hyperexecute-guided-walkthrough/
|
|
1263
|
+
#
|
|
1264
|
+
# Run with: ./hyperexecute --config hyperexecute.yaml
|
|
1265
|
+
# Download CLI: https://www.lambdatest.com/support/docs/hyperexecute-cli-run-tests-on-hyperexecute-grid/
|
|
1266
|
+
|
|
1267
|
+
version: "0.1"
|
|
1268
|
+
globalTimeout: 90
|
|
1269
|
+
testSuiteTimeout: 90
|
|
1270
|
+
testSuiteStep: 90
|
|
1271
|
+
|
|
1272
|
+
runson: ${runson}
|
|
1273
|
+
|
|
1274
|
+
autosplit: true
|
|
1275
|
+
retryOnFailure: false
|
|
1276
|
+
maxRetries: 1
|
|
1277
|
+
concurrency: 5
|
|
1278
|
+
|
|
1279
|
+
env:
|
|
1280
|
+
PLAYWRIGHT_BROWSERS_PATH: 0
|
|
1281
|
+
|
|
1282
|
+
pre:
|
|
1283
|
+
- npm install
|
|
1284
|
+
- npx playwright install ${ltBrowser === 'Chrome' ? 'chromium' : ltBrowser === 'pw-firefox' ? 'firefox' : 'webkit'}
|
|
1285
|
+
|
|
1286
|
+
testDiscovery:
|
|
1287
|
+
type: raw
|
|
1288
|
+
mode: dynamic
|
|
1289
|
+
command: grep -rn "test\\|it(" tests/*.spec.ts | cut -d':' -f1 | uniq
|
|
1290
|
+
|
|
1291
|
+
testRunnerCommand: npx playwright test $test --reporter=html
|
|
1292
|
+
|
|
1293
|
+
jobLabel:
|
|
1294
|
+
- qate
|
|
1295
|
+
- playwright
|
|
1296
|
+
- ${browser}
|
|
1297
|
+
|
|
1298
|
+
framework:
|
|
1299
|
+
name: playwright
|
|
1300
|
+
args:
|
|
1301
|
+
browserName: ${ltBrowser}
|
|
1302
|
+
platform: "${os === 'macos' || os === 'mac' ? 'MacOS' : 'Windows'} ${osVersion}"
|
|
1268
1303
|
`;
|
|
1269
1304
|
}
|
|
1270
1305
|
/**
|
|
@@ -1330,13 +1365,16 @@ function generateSummary(exportData) {
|
|
|
1330
1365
|
const stepCount = exportData.tests?.reduce((sum, t) => sum + (t.steps?.length || 0), 0) || 0;
|
|
1331
1366
|
const appName = exportData.application?.name || 'Unknown';
|
|
1332
1367
|
const appUrl = exportData.application?.url || 'Not specified';
|
|
1333
|
-
const type = exportData.type === 'testsequence' ? 'Test Sequence'
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1368
|
+
const type = exportData.type === 'testsequence' ? 'Test Sequence'
|
|
1369
|
+
: exportData.type === 'smart' ? 'Smart Generate'
|
|
1370
|
+
: 'Test Set';
|
|
1371
|
+
const name = exportData.testSequence?.name || exportData.testSet?.name
|
|
1372
|
+
|| (exportData.smartGenerate ? `PR #${exportData.smartGenerate.prNumber}` : 'Unknown');
|
|
1373
|
+
return `
|
|
1374
|
+
Generating Playwright tests from ${type}: "${name}"
|
|
1375
|
+
Application: ${appName} (${appUrl})
|
|
1376
|
+
Tests: ${testCount}
|
|
1377
|
+
Total Steps: ${stepCount}
|
|
1340
1378
|
`;
|
|
1341
1379
|
}
|
|
1342
1380
|
//# sourceMappingURL=PlaywrightGenerator.js.map
|