qa360 2.2.13 → 2.2.15
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QA360 Crawler Pack Generator
|
|
3
3
|
*
|
|
4
|
-
* Crawls a website and generates a complete pack.yml
|
|
4
|
+
* Crawls a website and generates a complete pack.yml in v2 format
|
|
5
5
|
*/
|
|
6
6
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
7
|
import { resolve, dirname } from 'path';
|
|
@@ -56,179 +56,53 @@ export async function generatePackFromCrawl(options) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Generate pack.yml YAML from crawl result
|
|
59
|
+
* Generate pack.yml YAML from crawl result (v2 format)
|
|
60
60
|
*/
|
|
61
61
|
function generatePackFromCrawlResult(crawlResult, options) {
|
|
62
62
|
const { siteMap, userJourneys, forms } = crawlResult;
|
|
63
63
|
const packName = options.packName || `${new URL(options.baseUrl).hostname.replace(/\./g, '-')}-tests`;
|
|
64
|
-
// Build
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const uiTests = userJourneys.map((journey, index) => ({
|
|
70
|
-
name: options.journeyNames?.[journey.name] || journey.name,
|
|
71
|
-
description: journey.description,
|
|
72
|
-
path: new URL(journey.entryPoint).pathname,
|
|
73
|
-
steps: journey.steps.map((step, stepIndex) => journeyStepToUiTestStep(step, stepIndex)),
|
|
74
|
-
}));
|
|
75
|
-
// Also add form-based tests
|
|
76
|
-
for (const form of forms) {
|
|
77
|
-
if (form.purpose !== 'login' && form.purpose !== 'signup') {
|
|
78
|
-
uiTests.push({
|
|
79
|
-
name: `${form.purpose.charAt(0).toUpperCase() + form.purpose.slice(1)} Form Test`,
|
|
80
|
-
description: `Test the ${form.purpose} form`,
|
|
81
|
-
path: siteMap.pages.find(p => p.elements.forms.includes(form))?.path || '/',
|
|
82
|
-
steps: generateFormTestSteps(form),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Generate YAML
|
|
87
|
-
let yaml = `# QA360 Pack - Generated by crawler
|
|
64
|
+
// Build full URLs for pages
|
|
65
|
+
const baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
66
|
+
const pages = siteMap.pages.map(p => `${baseUrl}${p.path.startsWith('/') ? '' : '/'}${p.path}`);
|
|
67
|
+
// Generate YAML (v2 format)
|
|
68
|
+
let yaml = `# QA360 Pack v2 - Generated by crawler
|
|
88
69
|
# Source: ${options.baseUrl}
|
|
89
70
|
# Generated: ${new Date().toISOString()}
|
|
90
71
|
|
|
91
|
-
version:
|
|
72
|
+
version: 2
|
|
92
73
|
name: "${packName}"
|
|
93
74
|
description: "Auto-generated E2E tests for ${new URL(options.baseUrl).hostname}"
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
web:
|
|
100
|
-
baseUrl: "${options.baseUrl}"
|
|
101
|
-
headless: true
|
|
102
|
-
screenshot: "only-on-fail"
|
|
103
|
-
video: "retain-on-fail"
|
|
76
|
+
execution:
|
|
77
|
+
default_timeout: 30000
|
|
78
|
+
default_retries: 2
|
|
79
|
+
on_failure: continue
|
|
104
80
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
gates:
|
|
82
|
+
ui:
|
|
83
|
+
adapter: playwright-ui
|
|
84
|
+
enabled: true
|
|
85
|
+
config:
|
|
86
|
+
baseUrl: "${options.baseUrl}"
|
|
87
|
+
pages:
|
|
88
|
+
${pages.map(p => ` - "${p}"`).join('\n')}
|
|
112
89
|
`;
|
|
113
|
-
|
|
90
|
+
// Add a11y gate if requested
|
|
91
|
+
if (options.includeA11y) {
|
|
114
92
|
yaml += `
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
93
|
+
a11y:
|
|
94
|
+
adapter: playwright-ui
|
|
95
|
+
enabled: true
|
|
96
|
+
config:
|
|
97
|
+
baseUrl: "${options.baseUrl}"
|
|
98
|
+
pages:
|
|
99
|
+
${pages.map(p => ` - "${p}"`).join('\n')}
|
|
100
|
+
budgets:
|
|
101
|
+
a11y_min: 80
|
|
118
102
|
`;
|
|
119
103
|
}
|
|
120
104
|
return yaml;
|
|
121
105
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Convert journey step to UI test step
|
|
124
|
-
*/
|
|
125
|
-
function journeyStepToUiTestStep(step, index) {
|
|
126
|
-
const uiStep = {
|
|
127
|
-
order: index + 1,
|
|
128
|
-
action: step.action,
|
|
129
|
-
description: step.description,
|
|
130
|
-
selector: step.selector,
|
|
131
|
-
value: step.value,
|
|
132
|
-
wait: 500,
|
|
133
|
-
};
|
|
134
|
-
if (step.expected) {
|
|
135
|
-
uiStep.expected = {};
|
|
136
|
-
if (step.expected.url)
|
|
137
|
-
uiStep.expected.url = step.expected.url;
|
|
138
|
-
if (step.expected.visible)
|
|
139
|
-
uiStep.expected.visible = step.expected.visible;
|
|
140
|
-
}
|
|
141
|
-
return uiStep;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Generate test steps for a form
|
|
145
|
-
*/
|
|
146
|
-
function generateFormTestSteps(form) {
|
|
147
|
-
const steps = [];
|
|
148
|
-
// Navigate to form
|
|
149
|
-
steps.push({
|
|
150
|
-
order: 1,
|
|
151
|
-
action: 'navigate',
|
|
152
|
-
description: 'Navigate to form',
|
|
153
|
-
value: form.selector,
|
|
154
|
-
});
|
|
155
|
-
// Fill form fields
|
|
156
|
-
for (const field of form.fields) {
|
|
157
|
-
if (field.required) {
|
|
158
|
-
const step = {
|
|
159
|
-
order: steps.length + 1,
|
|
160
|
-
action: field.inputType === 'select-one' ? 'select' : 'fill',
|
|
161
|
-
description: `Fill ${field.name || field.inputType}`,
|
|
162
|
-
selector: field.selector,
|
|
163
|
-
};
|
|
164
|
-
if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
|
|
165
|
-
step.value = field.options[0];
|
|
166
|
-
}
|
|
167
|
-
else if (field.inputType === 'email') {
|
|
168
|
-
step.value = 'test@example.com';
|
|
169
|
-
}
|
|
170
|
-
else if (field.inputType === 'tel') {
|
|
171
|
-
step.value = '+1234567890';
|
|
172
|
-
}
|
|
173
|
-
else if (field.inputType === 'checkbox') {
|
|
174
|
-
step.action = 'check';
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
step.value = 'Test value';
|
|
178
|
-
}
|
|
179
|
-
steps.push(step);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// Submit form
|
|
183
|
-
if (form.submitButton) {
|
|
184
|
-
steps.push({
|
|
185
|
-
order: steps.length + 1,
|
|
186
|
-
action: 'click',
|
|
187
|
-
description: 'Submit form',
|
|
188
|
-
selector: form.submitButton.selector,
|
|
189
|
-
wait: 1000,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
return steps;
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Escape a string value for YAML output
|
|
196
|
-
* Handles quotes, backslashes, and special characters
|
|
197
|
-
*/
|
|
198
|
-
function escapeYamlString(value) {
|
|
199
|
-
if (!value)
|
|
200
|
-
return '';
|
|
201
|
-
return value
|
|
202
|
-
.replace(/\\/g, '\\\\') // Backslashes first
|
|
203
|
-
.replace(/"/g, '\\"') // Double quotes
|
|
204
|
-
.replace(/\n/g, '\\n') // Newlines
|
|
205
|
-
.replace(/\r/g, '\\r') // Carriage returns
|
|
206
|
-
.replace(/\t/g, '\\t'); // Tabs
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Generate YAML for a UI test definition
|
|
210
|
-
*/
|
|
211
|
-
function generateUiTestYaml(test) {
|
|
212
|
-
const steps = test.steps.map(step => {
|
|
213
|
-
let yaml = ` - action: ${step.action}`;
|
|
214
|
-
if (step.description)
|
|
215
|
-
yaml += `\n description: "${escapeYamlString(step.description)}"`;
|
|
216
|
-
if (step.selector)
|
|
217
|
-
yaml += `\n selector: "${escapeYamlString(step.selector)}"`;
|
|
218
|
-
if (step.value)
|
|
219
|
-
yaml += `\n value: "${escapeYamlString(String(step.value))}"`;
|
|
220
|
-
if (step.expected)
|
|
221
|
-
yaml += `\n expected: ${JSON.stringify(step.expected)}`;
|
|
222
|
-
if (step.wait)
|
|
223
|
-
yaml += `\n wait: ${step.wait}`;
|
|
224
|
-
return yaml;
|
|
225
|
-
}).join('\n');
|
|
226
|
-
return ` - name: "${escapeYamlString(test.name)}"
|
|
227
|
-
description: "${escapeYamlString(test.description || '')}"
|
|
228
|
-
path: "${escapeYamlString(test.path || '/')}"
|
|
229
|
-
steps:
|
|
230
|
-
${steps}`;
|
|
231
|
-
}
|
|
232
106
|
/**
|
|
233
107
|
* Quick crawl - generates pack without detailed analysis
|
|
234
108
|
*/
|
|
@@ -41,6 +41,10 @@ export declare class PackMigrator {
|
|
|
41
41
|
* Migrate targets to gate configs
|
|
42
42
|
*/
|
|
43
43
|
private migrateTargetsToGates;
|
|
44
|
+
/**
|
|
45
|
+
* Transform relative page paths to full URLs
|
|
46
|
+
*/
|
|
47
|
+
private transformPagesToFullUrls;
|
|
44
48
|
/**
|
|
45
49
|
* Find a secret in environment variables by trying common names
|
|
46
50
|
*/
|
|
@@ -395,14 +395,64 @@ export class PackMigrator {
|
|
|
395
395
|
gate.config.baseUrl = v1Pack.targets.api.baseUrl;
|
|
396
396
|
gate.config.smoke = v1Pack.targets.api.smoke;
|
|
397
397
|
}
|
|
398
|
-
// Web target
|
|
399
|
-
if (v1Pack.targets.web
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
// Web target - migrate to ui, a11y, and accessibility gates
|
|
399
|
+
if (v1Pack.targets.web) {
|
|
400
|
+
const baseUrl = v1Pack.targets.web.baseUrl;
|
|
401
|
+
const pages = this.transformPagesToFullUrls(v1Pack.targets.web.pages || [], baseUrl);
|
|
402
|
+
// ui gate
|
|
403
|
+
if (v2Pack.gates['ui']) {
|
|
404
|
+
const gate = v2Pack.gates['ui'];
|
|
405
|
+
gate.config = gate.config || {};
|
|
406
|
+
gate.config.baseUrl = baseUrl;
|
|
407
|
+
gate.config.pages = pages;
|
|
408
|
+
}
|
|
409
|
+
// a11y gate (v1 name)
|
|
410
|
+
if (v2Pack.gates['a11y']) {
|
|
411
|
+
const gate = v2Pack.gates['a11y'];
|
|
412
|
+
gate.config = gate.config || {};
|
|
413
|
+
gate.config.baseUrl = baseUrl;
|
|
414
|
+
gate.config.pages = pages;
|
|
415
|
+
}
|
|
416
|
+
// accessibility gate (v2 sanitized name from 'a11y')
|
|
417
|
+
if (v2Pack.gates['accessibility']) {
|
|
418
|
+
const gate = v2Pack.gates['accessibility'];
|
|
419
|
+
gate.config = gate.config || {};
|
|
420
|
+
gate.config.baseUrl = baseUrl;
|
|
421
|
+
gate.config.pages = pages;
|
|
422
|
+
}
|
|
404
423
|
}
|
|
405
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Transform relative page paths to full URLs
|
|
427
|
+
*/
|
|
428
|
+
transformPagesToFullUrls(pages, baseUrl) {
|
|
429
|
+
if (!pages || pages.length === 0)
|
|
430
|
+
return [];
|
|
431
|
+
if (!baseUrl)
|
|
432
|
+
return pages;
|
|
433
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
434
|
+
return pages.map((page) => {
|
|
435
|
+
// Handle string pages (e.g., "/", "/products")
|
|
436
|
+
if (typeof page === 'string') {
|
|
437
|
+
if (page.startsWith('http')) {
|
|
438
|
+
return page; // Already a full URL
|
|
439
|
+
}
|
|
440
|
+
return `${base}${page.startsWith('/') ? '' : '/'}${page}`;
|
|
441
|
+
}
|
|
442
|
+
// Handle object pages (e.g., { url: "/", ... })
|
|
443
|
+
if (page && typeof page === 'object' && page.url) {
|
|
444
|
+
const url = page.url;
|
|
445
|
+
if (url.startsWith('http')) {
|
|
446
|
+
return page; // Already a full URL
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
...page,
|
|
450
|
+
url: `${base}${url.startsWith('/') ? '' : '/'}${url}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return page;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
406
456
|
/**
|
|
407
457
|
* Find a secret in environment variables by trying common names
|
|
408
458
|
*/
|
package/cli/package.json
CHANGED