@qate/cli 1.0.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 -0
- package/dist/AxiosGenerator.d.ts +24 -0
- package/dist/AxiosGenerator.d.ts.map +1 -0
- package/dist/AxiosGenerator.js +260 -0
- package/dist/AxiosGenerator.js.map +1 -0
- package/dist/PlaywrightGenerator.d.ts +39 -0
- package/dist/PlaywrightGenerator.d.ts.map +1 -0
- package/dist/PlaywrightGenerator.js +1342 -0
- package/dist/PlaywrightGenerator.js.map +1 -0
- package/dist/RestApiExecutor.d.ts +45 -0
- package/dist/RestApiExecutor.d.ts.map +1 -0
- package/dist/RestApiExecutor.js +336 -0
- package/dist/RestApiExecutor.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1269 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +296 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +143 -0
- package/dist/client.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Playwright Code Generator
|
|
4
|
+
* Converts Qate test definitions to runnable Playwright test files
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generatePlaywrightTests = generatePlaywrightTests;
|
|
8
|
+
exports.generateSummary = generateSummary;
|
|
9
|
+
/**
|
|
10
|
+
* Extract placeholder patterns ({{name}}) from a string
|
|
11
|
+
*/
|
|
12
|
+
function extractPlaceholdersFromString(str) {
|
|
13
|
+
if (!str)
|
|
14
|
+
return [];
|
|
15
|
+
const matches = str.match(/\{\{([^}]+)\}\}/g);
|
|
16
|
+
if (!matches)
|
|
17
|
+
return [];
|
|
18
|
+
return matches.map(m => m.replace(/\{\{|\}\}/g, ''));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract all unique placeholders from test steps
|
|
22
|
+
*/
|
|
23
|
+
function extractPlaceholders(test) {
|
|
24
|
+
const placeholders = new Set();
|
|
25
|
+
for (const step of test.steps || []) {
|
|
26
|
+
const input = step.toolInput || {};
|
|
27
|
+
// Check common input fields that may contain placeholders
|
|
28
|
+
const fieldsToCheck = ['value', 'text', 'url', 'expected', 'selector'];
|
|
29
|
+
for (const field of fieldsToCheck) {
|
|
30
|
+
if (typeof input[field] === 'string') {
|
|
31
|
+
for (const p of extractPlaceholdersFromString(input[field])) {
|
|
32
|
+
placeholders.add(p);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Also check nested objects (like verification)
|
|
37
|
+
if (step.verification && typeof step.verification === 'object') {
|
|
38
|
+
for (const key in step.verification) {
|
|
39
|
+
if (typeof step.verification[key] === 'string') {
|
|
40
|
+
for (const p of extractPlaceholdersFromString(step.verification[key])) {
|
|
41
|
+
placeholders.add(p);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Array.from(placeholders);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a string contains placeholder patterns
|
|
51
|
+
*/
|
|
52
|
+
function containsPlaceholder(str) {
|
|
53
|
+
if (!str)
|
|
54
|
+
return false;
|
|
55
|
+
return /\{\{[^}]+\}\}/.test(str);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wrap a value with resolve() if it contains placeholders, otherwise return quoted string
|
|
59
|
+
*/
|
|
60
|
+
function wrapValue(value, usePlaceholderResolve) {
|
|
61
|
+
if (!value)
|
|
62
|
+
return "''";
|
|
63
|
+
if (usePlaceholderResolve && containsPlaceholder(value)) {
|
|
64
|
+
return `resolve('${escapeString(value)}')`;
|
|
65
|
+
}
|
|
66
|
+
return `'${escapeString(value)}'`;
|
|
67
|
+
}
|
|
68
|
+
// Actions that trigger network/DOM changes and need settling
|
|
69
|
+
const SETTLING_ACTIONS = new Set([
|
|
70
|
+
'goTo', 'go_to', 'click', 'fill', 'type',
|
|
71
|
+
'selectOption', 'select_option', 'check', 'uncheck',
|
|
72
|
+
'doubleClick', 'double_click', 'rightClick', 'right_click',
|
|
73
|
+
'dragAndDrop', 'drag_and_drop', 'drag', 'evaluate', 'hover'
|
|
74
|
+
]);
|
|
75
|
+
/**
|
|
76
|
+
* Get the pure Playwright action code (without timing wrapper)
|
|
77
|
+
*/
|
|
78
|
+
function getActionCode(toolName, input, indent, usePlaceholderResolve) {
|
|
79
|
+
switch (toolName) {
|
|
80
|
+
case 'goTo':
|
|
81
|
+
case 'go_to':
|
|
82
|
+
return `${indent}await page.goto(${wrapValue(input.url, usePlaceholderResolve)});`;
|
|
83
|
+
case 'click':
|
|
84
|
+
return `${indent}await page.click('${escapeString(input.selector)}');`;
|
|
85
|
+
case 'fill':
|
|
86
|
+
return `${indent}await page.fill('${escapeString(input.selector)}', ${wrapValue(input.value, usePlaceholderResolve)});`;
|
|
87
|
+
case 'type':
|
|
88
|
+
const typeOpts = input.delay ? `, { delay: ${input.delay} }` : '';
|
|
89
|
+
return `${indent}await page.locator('${escapeString(input.selector)}').type(${wrapValue(input.text, usePlaceholderResolve)}${typeOpts});`;
|
|
90
|
+
case 'selectOption':
|
|
91
|
+
case 'select_option':
|
|
92
|
+
const values = Array.isArray(input.values) ? input.values : [input.values];
|
|
93
|
+
return `${indent}await page.selectOption('${escapeString(input.selector)}', ${JSON.stringify(values)});`;
|
|
94
|
+
case 'check':
|
|
95
|
+
return `${indent}await page.check('${escapeString(input.selector)}');`;
|
|
96
|
+
case 'uncheck':
|
|
97
|
+
return `${indent}await page.uncheck('${escapeString(input.selector)}');`;
|
|
98
|
+
case 'hover':
|
|
99
|
+
return `${indent}await page.hover('${escapeString(input.selector)}');`;
|
|
100
|
+
case 'doubleClick':
|
|
101
|
+
case 'double_click':
|
|
102
|
+
return `${indent}await page.dblclick('${escapeString(input.selector)}');`;
|
|
103
|
+
case 'rightClick':
|
|
104
|
+
case 'right_click':
|
|
105
|
+
return `${indent}await page.click('${escapeString(input.selector)}', { button: 'right' });`;
|
|
106
|
+
case 'dragAndDrop':
|
|
107
|
+
case 'drag_and_drop':
|
|
108
|
+
case 'drag':
|
|
109
|
+
return `${indent}await page.locator('${escapeString(input.sourceSelector)}').dragTo(page.locator('${escapeString(input.targetSelector)}'));`;
|
|
110
|
+
case 'evaluate':
|
|
111
|
+
return `${indent}await page.evaluate(() => { ${input.script} });`;
|
|
112
|
+
default:
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Wrap action with timing capture and error tracking for step result reporting
|
|
118
|
+
* Used for settling actions (click, fill, goTo, etc.) that need networkidle wait
|
|
119
|
+
*/
|
|
120
|
+
function wrapWithTiming(actionCode, stepId, stepIndex, indent, description) {
|
|
121
|
+
const varPrefix = `step_${stepIndex}`;
|
|
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();
|
|
138
|
+
${indent}qateRecordStepTiming('${stepId}', ${stepIndex}, '${escapedDescription}', 'passed', ${varPrefix}_activityStart, ${varPrefix}_activityEnd, ${varPrefix}_settledEnd);`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wrap action with step tracking (no networkidle wait)
|
|
142
|
+
* Used for non-settling actions like focus, press, scroll, assertions, etc.
|
|
143
|
+
*/
|
|
144
|
+
function wrapWithStepTracking(actionCode, stepId, stepIndex, indent, description) {
|
|
145
|
+
const varPrefix = `step_${stepIndex}`;
|
|
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;
|
|
159
|
+
${indent}}`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Convert a test step to Playwright code
|
|
163
|
+
*/
|
|
164
|
+
function stepToPlaywright(step, indent = ' ', usePlaceholderResolve = false, stepIndex = 0) {
|
|
165
|
+
const toolName = step.toolName?.replace('ui_automation--', '') || '';
|
|
166
|
+
const input = step.toolInput || {};
|
|
167
|
+
const stepId = step.id || `step_${stepIndex}`;
|
|
168
|
+
const description = step.description || `${toolName} ${input.selector || input.url || ''}`.trim();
|
|
169
|
+
// For actions that trigger network changes, wrap with timing capture
|
|
170
|
+
if (SETTLING_ACTIONS.has(toolName)) {
|
|
171
|
+
const actionCode = getActionCode(toolName, input, indent, usePlaceholderResolve);
|
|
172
|
+
if (actionCode) {
|
|
173
|
+
return wrapWithTiming(actionCode, stepId, stepIndex, indent, description);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Handle non-settling actions (assertions, waits, navigation info, etc.)
|
|
177
|
+
// Generate the raw action code first, then wrap with step tracking
|
|
178
|
+
let actionCode = null;
|
|
179
|
+
switch (toolName) {
|
|
180
|
+
case 'focus':
|
|
181
|
+
actionCode = `await page.focus('${escapeString(input.selector)}');`;
|
|
182
|
+
break;
|
|
183
|
+
case 'press':
|
|
184
|
+
case 'pressKey':
|
|
185
|
+
case 'press_key':
|
|
186
|
+
if (input.selector) {
|
|
187
|
+
actionCode = `await page.click('${escapeString(input.selector)}');\n await page.keyboard.press('${escapeString(input.key)}');`;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
actionCode = `await page.keyboard.press('${escapeString(input.key)}');`;
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'scroll':
|
|
194
|
+
const direction = input.direction || 'down';
|
|
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);
|
|
199
|
+
}, { dir: '${direction}', mult: ${multiplier} });`;
|
|
200
|
+
break;
|
|
201
|
+
case 'waitForSelector':
|
|
202
|
+
case 'wait_for_selector':
|
|
203
|
+
case 'waitForElement':
|
|
204
|
+
case 'wait_for_element':
|
|
205
|
+
const state = input.state || 'visible';
|
|
206
|
+
actionCode = `await page.waitForSelector('${escapeString(input.selector)}', { state: '${state}' });`;
|
|
207
|
+
break;
|
|
208
|
+
case 'assert':
|
|
209
|
+
actionCode = generateAssertionCode(input, usePlaceholderResolve);
|
|
210
|
+
break;
|
|
211
|
+
case 'screenshot':
|
|
212
|
+
actionCode = `await page.screenshot({ fullPage: ${input.fullPage || false} });`;
|
|
213
|
+
break;
|
|
214
|
+
case 'goBack':
|
|
215
|
+
case 'go_back':
|
|
216
|
+
actionCode = `await page.goBack();`;
|
|
217
|
+
break;
|
|
218
|
+
case 'goForward':
|
|
219
|
+
case 'go_forward':
|
|
220
|
+
actionCode = `await page.goForward();`;
|
|
221
|
+
break;
|
|
222
|
+
case 'reload':
|
|
223
|
+
actionCode = `await page.reload();`;
|
|
224
|
+
break;
|
|
225
|
+
case 'getTitle':
|
|
226
|
+
case 'get_title':
|
|
227
|
+
actionCode = `const title = await page.title();`;
|
|
228
|
+
break;
|
|
229
|
+
case 'getCurrentUrl':
|
|
230
|
+
case 'get_current_url':
|
|
231
|
+
actionCode = `const url = page.url();`;
|
|
232
|
+
break;
|
|
233
|
+
case 'getInnerHTML':
|
|
234
|
+
case 'get_inner_html':
|
|
235
|
+
actionCode = `const html = await page.locator('${escapeString(input.selector)}').innerHTML();`;
|
|
236
|
+
break;
|
|
237
|
+
case 'getInputValue':
|
|
238
|
+
case 'get_input_value':
|
|
239
|
+
actionCode = `const value = await page.locator('${escapeString(input.selector)}').inputValue();`;
|
|
240
|
+
break;
|
|
241
|
+
case 'setViewportSize':
|
|
242
|
+
case 'set_viewport_size':
|
|
243
|
+
actionCode = `await page.setViewportSize({ width: ${input.width}, height: ${input.height} });`;
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
// For unknown tools, add a comment (no step tracking)
|
|
247
|
+
return `${indent}// TODO: Unsupported tool '${toolName}' - ${JSON.stringify(input)}`;
|
|
248
|
+
}
|
|
249
|
+
// Wrap the action with step tracking
|
|
250
|
+
if (actionCode) {
|
|
251
|
+
return wrapWithStepTracking(`${indent}${actionCode}`, stepId, stepIndex, indent, description);
|
|
252
|
+
}
|
|
253
|
+
return `${indent}// No action generated for '${toolName}'`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Generate Playwright assertion code (raw, without indent prefix)
|
|
257
|
+
*/
|
|
258
|
+
function generateAssertionCode(input, usePlaceholderResolve = false) {
|
|
259
|
+
const { selector, assertion, expected, attribute, timeout } = input;
|
|
260
|
+
const timeoutOpt = timeout ? `{ timeout: ${timeout} }` : '';
|
|
261
|
+
switch (assertion) {
|
|
262
|
+
case 'toBeVisible':
|
|
263
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeVisible(${timeoutOpt});`;
|
|
264
|
+
case 'toBeHidden':
|
|
265
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeHidden();`;
|
|
266
|
+
case 'toBeEnabled':
|
|
267
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeEnabled();`;
|
|
268
|
+
case 'toBeDisabled':
|
|
269
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeDisabled();`;
|
|
270
|
+
case 'toHaveText':
|
|
271
|
+
return `await expect(page.locator('${escapeString(selector)}')).toHaveText(${wrapValue(expected, usePlaceholderResolve)});`;
|
|
272
|
+
case 'toContainText':
|
|
273
|
+
return `await expect(page.locator('${escapeString(selector)}')).toContainText(${wrapValue(expected, usePlaceholderResolve)});`;
|
|
274
|
+
case 'toHaveValue':
|
|
275
|
+
return `await expect(page.locator('${escapeString(selector)}')).toHaveValue(${wrapValue(expected, usePlaceholderResolve)});`;
|
|
276
|
+
case 'toHaveAttribute':
|
|
277
|
+
return `await expect(page.locator('${escapeString(selector)}')).toHaveAttribute('${escapeString(attribute)}', ${wrapValue(expected, usePlaceholderResolve)});`;
|
|
278
|
+
case 'toHaveCount':
|
|
279
|
+
return `await expect(page.locator('${escapeString(selector)}')).toHaveCount(${expected});`;
|
|
280
|
+
case 'toBeChecked':
|
|
281
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeChecked();`;
|
|
282
|
+
case 'toBeUnchecked':
|
|
283
|
+
return `await expect(page.locator('${escapeString(selector)}')).not.toBeChecked();`;
|
|
284
|
+
case 'toBeEditable':
|
|
285
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeEditable();`;
|
|
286
|
+
case 'toBeFocused':
|
|
287
|
+
return `await expect(page.locator('${escapeString(selector)}')).toBeFocused();`;
|
|
288
|
+
case 'toHaveURL':
|
|
289
|
+
return `await expect(page).toHaveURL(${typeof expected === 'string' ? wrapValue(expected, usePlaceholderResolve) : expected});`;
|
|
290
|
+
case 'toHaveTitle':
|
|
291
|
+
return `await expect(page).toHaveTitle(${typeof expected === 'string' ? wrapValue(expected, usePlaceholderResolve) : expected});`;
|
|
292
|
+
default:
|
|
293
|
+
return `// TODO: Unsupported assertion '${assertion}'`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Escape string for use in generated code
|
|
298
|
+
*/
|
|
299
|
+
function escapeString(str) {
|
|
300
|
+
if (!str)
|
|
301
|
+
return '';
|
|
302
|
+
return str
|
|
303
|
+
.replace(/\\/g, '\\\\')
|
|
304
|
+
.replace(/'/g, "\\'")
|
|
305
|
+
.replace(/\n/g, '\\n')
|
|
306
|
+
.replace(/\r/g, '\\r');
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Convert test name to valid filename
|
|
310
|
+
*/
|
|
311
|
+
function toFilename(name) {
|
|
312
|
+
return name
|
|
313
|
+
.toLowerCase()
|
|
314
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
315
|
+
.replace(/^-|-$/g, '')
|
|
316
|
+
.substring(0, 50);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Generate a single test file
|
|
320
|
+
*/
|
|
321
|
+
function generateTestFile(test, baseUrl) {
|
|
322
|
+
const placeholders = extractPlaceholders(test);
|
|
323
|
+
const hasPlaceholders = placeholders.length > 0;
|
|
324
|
+
// Filter and sort steps, then map with index for timing capture
|
|
325
|
+
const filteredSteps = (test.steps || [])
|
|
326
|
+
.filter(s => s.type === 'tool_use')
|
|
327
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
328
|
+
const steps = filteredSteps
|
|
329
|
+
.map((step, index) => stepToPlaywright(step, ' ', hasPlaceholders, index))
|
|
330
|
+
.join('\n\n');
|
|
331
|
+
const description = test.description ? `\n * ${test.description}` : '';
|
|
332
|
+
const tags = test.tags?.length ? `\n * Tags: ${test.tags.join(', ')}` : '';
|
|
333
|
+
const priority = test.priority ? `\n * Priority: ${test.priority}` : '';
|
|
334
|
+
// Generate import statements
|
|
335
|
+
const imports = [`import { test, expect } from '@playwright/test';`];
|
|
336
|
+
imports.push(`import { qateRecordStepTiming, qateGetAndClearStepTimings, qateSetupNetworkCapture } from '../qate-runtime';`);
|
|
337
|
+
if (hasPlaceholders) {
|
|
338
|
+
imports.push(`import { qateResolve, replacePlaceholders } from '../qate-runtime';`);
|
|
339
|
+
}
|
|
340
|
+
// Generate test context and resolver setup for placeholder tests
|
|
341
|
+
let contextSetup = '';
|
|
342
|
+
if (hasPlaceholders) {
|
|
343
|
+
const testContext = {
|
|
344
|
+
testId: test.id?.toString() || '',
|
|
345
|
+
testName: test.name,
|
|
346
|
+
datasetId: test.datasetId || null,
|
|
347
|
+
authRoleId: test.authRoleId || null,
|
|
348
|
+
placeholders: placeholders,
|
|
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);
|
|
355
|
+
`;
|
|
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
|
+
});
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Convert user-friendly OS name to BrowserStack format
|
|
389
|
+
*/
|
|
390
|
+
function toBrowserStackOS(os) {
|
|
391
|
+
const osLower = os.toLowerCase();
|
|
392
|
+
if (osLower === 'macos' || osLower === 'mac' || osLower === 'osx') {
|
|
393
|
+
return 'OS X';
|
|
394
|
+
}
|
|
395
|
+
if (osLower === 'windows' || osLower === 'win') {
|
|
396
|
+
return 'Windows';
|
|
397
|
+
}
|
|
398
|
+
return 'Windows'; // default
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Convert user-friendly browser name to BrowserStack format
|
|
402
|
+
*/
|
|
403
|
+
function toBrowserStackBrowser(browser) {
|
|
404
|
+
const browserLower = browser.toLowerCase();
|
|
405
|
+
if (browserLower === 'safari' || browserLower === 'webkit') {
|
|
406
|
+
return 'playwright-webkit';
|
|
407
|
+
}
|
|
408
|
+
if (browserLower === 'firefox' || browserLower === 'ff') {
|
|
409
|
+
return 'playwright-firefox';
|
|
410
|
+
}
|
|
411
|
+
if (browserLower === 'edge') {
|
|
412
|
+
return 'edge';
|
|
413
|
+
}
|
|
414
|
+
return 'chrome'; // default
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Convert user-friendly browser name to LambdaTest format
|
|
418
|
+
*/
|
|
419
|
+
function toLambdaTestBrowser(browser) {
|
|
420
|
+
const browserLower = browser.toLowerCase();
|
|
421
|
+
if (browserLower === 'safari' || browserLower === 'webkit') {
|
|
422
|
+
return 'pw-webkit';
|
|
423
|
+
}
|
|
424
|
+
if (browserLower === 'firefox' || browserLower === 'ff') {
|
|
425
|
+
return 'pw-firefox';
|
|
426
|
+
}
|
|
427
|
+
if (browserLower === 'edge') {
|
|
428
|
+
return 'MicrosoftEdge';
|
|
429
|
+
}
|
|
430
|
+
return 'Chrome'; // default
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Convert user-friendly OS to LambdaTest platform format
|
|
434
|
+
*/
|
|
435
|
+
function toLambdaTestPlatform(os, osVersion) {
|
|
436
|
+
const osLower = os.toLowerCase();
|
|
437
|
+
if (osLower === 'macos' || osLower === 'mac' || osLower === 'osx') {
|
|
438
|
+
return `MacOS ${osVersion || 'Sonoma'}`;
|
|
439
|
+
}
|
|
440
|
+
if (osLower === 'windows' || osLower === 'win') {
|
|
441
|
+
return `Windows ${osVersion || '11'}`;
|
|
442
|
+
}
|
|
443
|
+
return `Windows ${osVersion || '11'}`; // default
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Generate playwright.config.ts
|
|
447
|
+
*/
|
|
448
|
+
function generateConfig(options, appUrl) {
|
|
449
|
+
const { provider = 'local', browsers = ['chromium'], browser = 'chrome', browserVersion = 'latest', os = 'windows', osVersion = '11', orchestration = 'websocket' } = options;
|
|
450
|
+
let projects = '';
|
|
451
|
+
let connectOptions = '';
|
|
452
|
+
// Determine effective orchestration mode
|
|
453
|
+
// Sauce Labs only supports CLI orchestration
|
|
454
|
+
const effectiveOrchestration = provider === 'saucelabs' ? 'cli' : orchestration;
|
|
455
|
+
if (provider === 'browserstack' && effectiveOrchestration === 'websocket') {
|
|
456
|
+
const bsBrowser = toBrowserStackBrowser(browser);
|
|
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
|
+
},
|
|
478
|
+
},`;
|
|
479
|
+
}
|
|
480
|
+
else if (provider === 'browserstack' && effectiveOrchestration === 'cli') {
|
|
481
|
+
// BrowserStack CLI orchestration - no connectOptions needed
|
|
482
|
+
connectOptions = `
|
|
483
|
+
// BrowserStack configuration (CLI orchestration)
|
|
484
|
+
// Run with: npx browserstack-node-sdk playwright test
|
|
485
|
+
// Docs: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs`;
|
|
486
|
+
}
|
|
487
|
+
else if (provider === 'saucelabs') {
|
|
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
|
|
494
|
+
// Docs: https://docs.saucelabs.com/web-apps/automated-testing/playwright/`;
|
|
495
|
+
}
|
|
496
|
+
else if (provider === 'lambdatest' && effectiveOrchestration === 'websocket') {
|
|
497
|
+
const ltBrowser = toLambdaTestBrowser(browser);
|
|
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
|
+
},
|
|
519
|
+
},`;
|
|
520
|
+
}
|
|
521
|
+
else if (provider === 'lambdatest' && effectiveOrchestration === 'cli') {
|
|
522
|
+
// LambdaTest CLI orchestration - no connectOptions needed
|
|
523
|
+
connectOptions = `
|
|
524
|
+
// LambdaTest HyperExecute configuration (CLI orchestration)
|
|
525
|
+
// Run with: ./hyperexecute --config hyperexecute.yaml
|
|
526
|
+
// Docs: https://www.lambdatest.com/support/docs/hyperexecute-guided-walkthrough/`;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Local provider
|
|
530
|
+
const browserProjects = browsers.map(browser => `
|
|
531
|
+
{
|
|
532
|
+
name: '${browser}',
|
|
533
|
+
use: { ...devices['Desktop Chrome'] },
|
|
534
|
+
}`).join(',');
|
|
535
|
+
projects = `
|
|
536
|
+
projects: [${browserProjects}
|
|
537
|
+
],`;
|
|
538
|
+
}
|
|
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
|
+
['./qate-reporter.ts'],
|
|
558
|
+
],
|
|
559
|
+
|
|
560
|
+
use: {
|
|
561
|
+
baseURL: '${appUrl}',
|
|
562
|
+
trace: 'on-first-retry',
|
|
563
|
+
screenshot: 'only-on-failure',
|
|
564
|
+
},
|
|
565
|
+
${connectOptions}${projects}
|
|
566
|
+
// Global timeout
|
|
567
|
+
timeout: 30000,
|
|
568
|
+
expect: {
|
|
569
|
+
timeout: 5000,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Generate package.json
|
|
576
|
+
*/
|
|
577
|
+
function generatePackageJson(name, provider = 'local') {
|
|
578
|
+
const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
579
|
+
const scripts = {
|
|
580
|
+
test: 'playwright test',
|
|
581
|
+
'test:headed': 'playwright test --headed',
|
|
582
|
+
'test:debug': 'playwright test --debug',
|
|
583
|
+
report: 'playwright show-report',
|
|
584
|
+
};
|
|
585
|
+
const devDependencies = {
|
|
586
|
+
'@playwright/test': '^1.40.0',
|
|
587
|
+
};
|
|
588
|
+
// Add saucectl for Sauce Labs
|
|
589
|
+
if (provider === 'saucelabs') {
|
|
590
|
+
scripts.test = 'saucectl run';
|
|
591
|
+
scripts['test:local'] = 'playwright test';
|
|
592
|
+
}
|
|
593
|
+
return JSON.stringify({
|
|
594
|
+
name: `${safeName}-e2e-tests`,
|
|
595
|
+
version: '1.0.0',
|
|
596
|
+
description: `E2E tests generated by Qate CLI`,
|
|
597
|
+
scripts,
|
|
598
|
+
devDependencies,
|
|
599
|
+
}, null, 2);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Generate Qate reporter for sending results back to Qate
|
|
603
|
+
*/
|
|
604
|
+
function generateQateReporter(testMapping, options) {
|
|
605
|
+
const testMappingJson = JSON.stringify(testMapping, null, 2);
|
|
606
|
+
const executionId = options.executionId || '';
|
|
607
|
+
const apiUrl = options.apiUrl || 'https://api.qate.ai';
|
|
608
|
+
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 type {
|
|
617
|
+
Reporter,
|
|
618
|
+
FullConfig,
|
|
619
|
+
Suite,
|
|
620
|
+
TestCase,
|
|
621
|
+
TestResult,
|
|
622
|
+
FullResult,
|
|
623
|
+
} from '@playwright/test/reporter';
|
|
624
|
+
import type { StepTiming } from './qate-runtime';
|
|
625
|
+
|
|
626
|
+
// Qate configuration (hardcoded at generation time)
|
|
627
|
+
const EXECUTION_ID = '${executionId}';
|
|
628
|
+
const API_URL = '${apiUrl}';
|
|
629
|
+
const PROVIDER = '${provider}';
|
|
630
|
+
|
|
631
|
+
// Test ID mapping from Qate
|
|
632
|
+
const TEST_MAPPING: { [filename: string]: { testId: string; testName: string } } = ${testMappingJson};
|
|
633
|
+
|
|
634
|
+
interface StepNetworkRequest {
|
|
635
|
+
url: string;
|
|
636
|
+
method: string;
|
|
637
|
+
status?: number;
|
|
638
|
+
contentType?: string;
|
|
639
|
+
duration?: number;
|
|
640
|
+
failure?: string;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
interface StepResult {
|
|
644
|
+
stepId?: string;
|
|
645
|
+
stepIndex: number;
|
|
646
|
+
description: string;
|
|
647
|
+
status: 'passed' | 'failed' | 'error' | 'skipped';
|
|
648
|
+
duration: number;
|
|
649
|
+
error?: string;
|
|
650
|
+
performanceData?: {
|
|
651
|
+
activityStart: string;
|
|
652
|
+
activityEnd: string;
|
|
653
|
+
activityDuration: number;
|
|
654
|
+
activityDurationFormatted: string;
|
|
655
|
+
settledEnd?: string;
|
|
656
|
+
settledDuration?: number;
|
|
657
|
+
settledDurationFormatted?: string;
|
|
658
|
+
};
|
|
659
|
+
networkRequests?: StepNetworkRequest[];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
interface TestResultData {
|
|
663
|
+
testId: string;
|
|
664
|
+
testName: string;
|
|
665
|
+
status: 'passed' | 'failed' | 'error';
|
|
666
|
+
duration: number;
|
|
667
|
+
startedAt: string;
|
|
668
|
+
steps: StepResult[];
|
|
669
|
+
error?: string;
|
|
670
|
+
metadata?: Record<string, unknown>;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
class QateReporter implements Reporter {
|
|
674
|
+
private results: TestResultData[] = [];
|
|
675
|
+
private startTime: Date = new Date();
|
|
676
|
+
private config: FullConfig | null = null;
|
|
677
|
+
|
|
678
|
+
onBegin(config: FullConfig, suite: Suite) {
|
|
679
|
+
this.config = config;
|
|
680
|
+
this.startTime = new Date();
|
|
681
|
+
console.log('\\n[Qate] Starting test execution...');
|
|
682
|
+
console.log('[Qate] Execution ID:', EXECUTION_ID || 'not set');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
onTestEnd(test: TestCase, result: TestResult) {
|
|
686
|
+
// Find test mapping by filename
|
|
687
|
+
const filePath = test.location.file;
|
|
688
|
+
const filename = filePath.split('/').pop()?.split('\\\\').pop() || '';
|
|
689
|
+
const mapping = TEST_MAPPING[filename];
|
|
690
|
+
|
|
691
|
+
if (!mapping) {
|
|
692
|
+
console.warn('[Qate] No mapping found for test file:', filename);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Get step timings from test attachments (cross-process communication from worker)
|
|
697
|
+
let collectedStepTimings: StepTiming[] = [];
|
|
698
|
+
const stepTimingsAttachment = result.attachments.find(a => a.name === 'qate-step-timings');
|
|
699
|
+
if (stepTimingsAttachment?.body) {
|
|
700
|
+
try {
|
|
701
|
+
collectedStepTimings = JSON.parse(stepTimingsAttachment.body.toString());
|
|
702
|
+
} catch (e) {
|
|
703
|
+
console.warn('[Qate] Failed to parse step timings attachment');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Helper to format duration
|
|
708
|
+
const formatDuration = (ms: number): string => {
|
|
709
|
+
if (ms < 1000) return \`\${Math.round(ms)}ms\`;
|
|
710
|
+
if (ms < 60000) return \`\${(ms / 1000).toFixed(2)}s\`;
|
|
711
|
+
const minutes = Math.floor(ms / 60000);
|
|
712
|
+
const seconds = ((ms % 60000) / 1000).toFixed(1);
|
|
713
|
+
return \`\${minutes}m \${seconds}s\`;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// Build steps from stepTimings (collected via qateRecordStepTiming calls in generated test code)
|
|
717
|
+
// This is more reliable than result.steps which only contains steps defined via test.step()
|
|
718
|
+
const steps: StepResult[] = collectedStepTimings
|
|
719
|
+
.sort((a, b) => a.stepIndex - b.stepIndex)
|
|
720
|
+
.map((timing) => {
|
|
721
|
+
const stepResult: StepResult = {
|
|
722
|
+
stepId: timing.stepId,
|
|
723
|
+
stepIndex: timing.stepIndex,
|
|
724
|
+
description: timing.description,
|
|
725
|
+
status: timing.status,
|
|
726
|
+
duration: timing.settledDuration,
|
|
727
|
+
error: timing.error,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Add performance data
|
|
731
|
+
stepResult.performanceData = {
|
|
732
|
+
activityStart: new Date(timing.activityStart).toISOString(),
|
|
733
|
+
activityEnd: new Date(timing.activityEnd).toISOString(),
|
|
734
|
+
activityDuration: timing.activityDuration,
|
|
735
|
+
activityDurationFormatted: formatDuration(timing.activityDuration),
|
|
736
|
+
settledEnd: new Date(timing.settledEnd).toISOString(),
|
|
737
|
+
settledDuration: timing.settledDuration,
|
|
738
|
+
settledDurationFormatted: formatDuration(timing.settledDuration),
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// Add network requests if captured
|
|
742
|
+
if (timing.networkRequests && timing.networkRequests.length > 0) {
|
|
743
|
+
stepResult.networkRequests = timing.networkRequests;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return stepResult;
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Log step count for debugging
|
|
750
|
+
console.log(\`[Qate] Collected \${steps.length} step results from attachment\`);
|
|
751
|
+
|
|
752
|
+
const testResult: TestResultData = {
|
|
753
|
+
testId: mapping.testId,
|
|
754
|
+
testName: mapping.testName,
|
|
755
|
+
status: result.status === 'passed' ? 'passed' : result.status === 'timedOut' ? 'error' : 'failed',
|
|
756
|
+
duration: result.duration,
|
|
757
|
+
startedAt: result.startTime.toISOString(),
|
|
758
|
+
steps,
|
|
759
|
+
error: result.error?.message,
|
|
760
|
+
metadata: {
|
|
761
|
+
retries: result.retry,
|
|
762
|
+
workerIndex: result.workerIndex,
|
|
763
|
+
attachments: result.attachments.map(a => ({ name: a.name, contentType: a.contentType })),
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
this.results.push(testResult);
|
|
768
|
+
|
|
769
|
+
const statusIcon = testResult.status === 'passed' ? '✓' : '✗';
|
|
770
|
+
console.log(\`[Qate] \${statusIcon} \${mapping.testName} (\${testResult.duration}ms)\`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async onEnd(result: FullResult) {
|
|
774
|
+
const apiKey = process.env.QATE_API_KEY;
|
|
775
|
+
|
|
776
|
+
if (!EXECUTION_ID) {
|
|
777
|
+
console.log('\\n[Qate] No execution ID configured - skipping result reporting');
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (!apiKey) {
|
|
782
|
+
console.log('\\n[Qate] QATE_API_KEY not set - skipping result reporting');
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
console.log('\\n[Qate] Reporting results to Qate...');
|
|
787
|
+
|
|
788
|
+
const payload = {
|
|
789
|
+
executionId: EXECUTION_ID,
|
|
790
|
+
status: result.status,
|
|
791
|
+
results: this.results,
|
|
792
|
+
summary: {
|
|
793
|
+
total: this.results.length,
|
|
794
|
+
passed: this.results.filter(r => r.status === 'passed').length,
|
|
795
|
+
failed: this.results.filter(r => r.status === 'failed').length,
|
|
796
|
+
error: this.results.filter(r => r.status === 'error').length,
|
|
797
|
+
status: result.status,
|
|
798
|
+
},
|
|
799
|
+
startedAt: this.startTime.toISOString(),
|
|
800
|
+
completedAt: new Date().toISOString(),
|
|
801
|
+
provider: PROVIDER,
|
|
802
|
+
environment: {
|
|
803
|
+
browser: this.config?.projects?.[0]?.name || 'unknown',
|
|
804
|
+
os: process.platform,
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const controller = new AbortController();
|
|
810
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
811
|
+
|
|
812
|
+
const response = await fetch(\`\${API_URL}/ci/report\`, {
|
|
813
|
+
method: 'POST',
|
|
814
|
+
headers: {
|
|
815
|
+
'Content-Type': 'application/json',
|
|
816
|
+
'X-API-Key': apiKey,
|
|
817
|
+
},
|
|
818
|
+
body: JSON.stringify(payload),
|
|
819
|
+
signal: controller.signal,
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
clearTimeout(timeoutId);
|
|
823
|
+
|
|
824
|
+
if (response.ok) {
|
|
825
|
+
const data = await response.json();
|
|
826
|
+
console.log('[Qate] Results reported successfully');
|
|
827
|
+
console.log(\`[Qate] Summary: \${data.summary?.passed || 0} passed, \${data.summary?.failed || 0} failed\`);
|
|
828
|
+
console.log(\`[Qate] View results at: https://app.qate.ai/executions/\${EXECUTION_ID}\`);
|
|
829
|
+
} else {
|
|
830
|
+
const error = await response.text();
|
|
831
|
+
console.error('[Qate] Failed to report results:', response.status, error);
|
|
832
|
+
}
|
|
833
|
+
} catch (error: any) {
|
|
834
|
+
if (error.name === 'AbortError') {
|
|
835
|
+
console.error('[Qate] Request timed out after 30 seconds');
|
|
836
|
+
} else {
|
|
837
|
+
console.error('[Qate] Error reporting results:', error.message || error);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Small delay to ensure HTTP connection is properly closed before Playwright cleanup
|
|
842
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export default QateReporter;
|
|
847
|
+
`;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Generate qate-runtime.ts helper for placeholder resolution
|
|
851
|
+
*/
|
|
852
|
+
function generateQateRuntime(apiUrl, executionId) {
|
|
853
|
+
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
|
+
}
|
|
1113
|
+
`;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Convert user-friendly browser name to Sauce Labs format
|
|
1117
|
+
*/
|
|
1118
|
+
function toSauceLabsBrowser(browser) {
|
|
1119
|
+
const browserLower = browser.toLowerCase();
|
|
1120
|
+
if (browserLower === 'safari' || browserLower === 'webkit') {
|
|
1121
|
+
return 'webkit';
|
|
1122
|
+
}
|
|
1123
|
+
if (browserLower === 'firefox' || browserLower === 'ff') {
|
|
1124
|
+
return 'firefox';
|
|
1125
|
+
}
|
|
1126
|
+
if (browserLower === 'edge') {
|
|
1127
|
+
return 'chromium'; // Sauce Labs uses chromium for Edge
|
|
1128
|
+
}
|
|
1129
|
+
return 'chromium'; // default
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Convert user-friendly OS to Sauce Labs platform format
|
|
1133
|
+
*/
|
|
1134
|
+
function toSauceLabsPlatform(os, osVersion) {
|
|
1135
|
+
const osLower = os.toLowerCase();
|
|
1136
|
+
if (osLower === 'macos' || osLower === 'mac' || osLower === 'osx') {
|
|
1137
|
+
return `macOS ${osVersion || '13'}`;
|
|
1138
|
+
}
|
|
1139
|
+
if (osLower === 'windows' || osLower === 'win') {
|
|
1140
|
+
return `Windows ${osVersion || '11'}`;
|
|
1141
|
+
}
|
|
1142
|
+
return `Windows ${osVersion || '11'}`; // default
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Generate Sauce Labs saucectl config.yml
|
|
1146
|
+
*/
|
|
1147
|
+
function generateSauceConfig(appUrl, options) {
|
|
1148
|
+
const { browser = 'chrome', os = 'windows', osVersion = '11' } = options;
|
|
1149
|
+
const slBrowser = toSauceLabsBrowser(browser);
|
|
1150
|
+
const slPlatform = toSauceLabsPlatform(os, osVersion);
|
|
1151
|
+
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
|
|
1183
|
+
`;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Generate BrowserStack browserstack.yml config
|
|
1187
|
+
*/
|
|
1188
|
+
function generateBrowserStackConfig(options) {
|
|
1189
|
+
const { browser = 'chrome', browserVersion = 'latest', os = 'windows', osVersion = '11' } = options;
|
|
1190
|
+
const bsBrowser = toBrowserStackBrowser(browser);
|
|
1191
|
+
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
|
|
1217
|
+
`;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Generate LambdaTest HyperExecute YAML config
|
|
1221
|
+
*/
|
|
1222
|
+
function generateHyperExecuteConfig(options) {
|
|
1223
|
+
const { browser = 'chrome', os = 'windows', osVersion = '11' } = options;
|
|
1224
|
+
const ltBrowser = toLambdaTestBrowser(browser);
|
|
1225
|
+
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}"
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Main generator function
|
|
1272
|
+
*/
|
|
1273
|
+
function generatePlaywrightTests(exportData, options) {
|
|
1274
|
+
const appUrl = exportData.application?.url || 'http://localhost:3000';
|
|
1275
|
+
const appName = exportData.application?.name || 'app';
|
|
1276
|
+
const tests = exportData.tests || [];
|
|
1277
|
+
const provider = options.provider || 'local';
|
|
1278
|
+
const result = {
|
|
1279
|
+
'playwright.config.ts': generateConfig(options, appUrl),
|
|
1280
|
+
'package.json': generatePackageJson(appName, provider),
|
|
1281
|
+
tests: {},
|
|
1282
|
+
};
|
|
1283
|
+
// Build test mapping for Qate reporter (filename -> testId/testName)
|
|
1284
|
+
const testMapping = {};
|
|
1285
|
+
// Check if any tests have placeholders
|
|
1286
|
+
let anyTestHasPlaceholders = false;
|
|
1287
|
+
// Generate test files
|
|
1288
|
+
for (const test of tests) {
|
|
1289
|
+
const filename = `${toFilename(test.name)}.spec.ts`;
|
|
1290
|
+
result.tests[filename] = generateTestFile(test, appUrl);
|
|
1291
|
+
// Add to test mapping for reporter
|
|
1292
|
+
testMapping[filename] = {
|
|
1293
|
+
testId: test.id?.toString() || '',
|
|
1294
|
+
testName: test.name,
|
|
1295
|
+
};
|
|
1296
|
+
// Check for placeholders in this test
|
|
1297
|
+
if (extractPlaceholders(test).length > 0) {
|
|
1298
|
+
anyTestHasPlaceholders = true;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
// Generate qate-runtime.ts - always needed for step timing functions used by reporter
|
|
1302
|
+
const apiUrl = options.apiUrl || 'https://api.qate.ai';
|
|
1303
|
+
result['qate-runtime.ts'] = generateQateRuntime(apiUrl, options.executionId);
|
|
1304
|
+
// Determine effective orchestration mode
|
|
1305
|
+
const orchestration = options.orchestration || 'websocket';
|
|
1306
|
+
const effectiveOrchestration = provider === 'saucelabs' ? 'cli' : orchestration;
|
|
1307
|
+
// Add provider-specific config files for CLI orchestration
|
|
1308
|
+
if (provider === 'saucelabs') {
|
|
1309
|
+
result['.sauce/config.yml'] = generateSauceConfig(appUrl, options);
|
|
1310
|
+
}
|
|
1311
|
+
else if (provider === 'browserstack' && effectiveOrchestration === 'cli') {
|
|
1312
|
+
result['browserstack.yml'] = generateBrowserStackConfig(options);
|
|
1313
|
+
}
|
|
1314
|
+
else if (provider === 'lambdatest' && effectiveOrchestration === 'cli') {
|
|
1315
|
+
result['hyperexecute.yaml'] = generateHyperExecuteConfig(options);
|
|
1316
|
+
}
|
|
1317
|
+
// Generate Qate reporter for result reporting
|
|
1318
|
+
result['qate-reporter.ts'] = generateQateReporter(testMapping, {
|
|
1319
|
+
executionId: options.executionId,
|
|
1320
|
+
apiUrl,
|
|
1321
|
+
provider: options.provider,
|
|
1322
|
+
});
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Generate a summary of what will be created
|
|
1327
|
+
*/
|
|
1328
|
+
function generateSummary(exportData) {
|
|
1329
|
+
const testCount = exportData.tests?.length || 0;
|
|
1330
|
+
const stepCount = exportData.tests?.reduce((sum, t) => sum + (t.steps?.length || 0), 0) || 0;
|
|
1331
|
+
const appName = exportData.application?.name || 'Unknown';
|
|
1332
|
+
const appUrl = exportData.application?.url || 'Not specified';
|
|
1333
|
+
const type = exportData.type === 'testsequence' ? 'Test Sequence' : 'Test Set';
|
|
1334
|
+
const name = exportData.testSequence?.name || exportData.testSet?.name || 'Unknown';
|
|
1335
|
+
return `
|
|
1336
|
+
Generating Playwright tests from ${type}: "${name}"
|
|
1337
|
+
Application: ${appName} (${appUrl})
|
|
1338
|
+
Tests: ${testCount}
|
|
1339
|
+
Total Steps: ${stepCount}
|
|
1340
|
+
`;
|
|
1341
|
+
}
|
|
1342
|
+
//# sourceMappingURL=PlaywrightGenerator.js.map
|