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 with E2E tests
4
+ * Crawls a website and generates a complete pack.yml in v2 format
5
5
  */
6
6
  import type { CrawlOptions, CrawlResult } from '../crawler/index.js';
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QA360 Crawler Pack Generator
3
3
  *
4
- * Crawls a website and generates a complete pack.yml with E2E tests
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 gates array
65
- const gates = ['ui'];
66
- if (options.includeA11y)
67
- gates.push('a11y');
68
- // Build UI tests from user journeys
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: 1
72
+ version: 2
92
73
  name: "${packName}"
93
74
  description: "Auto-generated E2E tests for ${new URL(options.baseUrl).hostname}"
94
75
 
95
- gates:
96
- ${gates.map(g => ` - ${g}`).join('\n')}
97
-
98
- targets:
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
- # Pages discovered by crawler
106
- pages:
107
- ${siteMap.pages.map(p => ` - "${p.path}"`).join('\n')}
108
-
109
- # E2E UI tests generated from discovered user journeys
110
- uiTests:
111
- ${uiTests.map(test => generateUiTestYaml(test)).join('\n')}
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
- if (siteMap.metadata.pagesCrawled > 0) {
90
+ // Add a11y gate if requested
91
+ if (options.includeA11y) {
114
92
  yaml += `
115
- budgets:
116
- a11y_min: 80
117
- perf_p95_ms: ${Math.round(siteMap.metadata.avgLoadTime * 1.5)}
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 && v2Pack.gates['ui']) {
400
- const gate = v2Pack.gates['ui'];
401
- gate.config = gate.config || {};
402
- gate.config.baseUrl = v1Pack.targets.web.baseUrl;
403
- gate.config.pages = v1Pack.targets.web.pages;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.13",
3
+ "version": "2.2.15",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.13",
3
+ "version": "2.2.15",
4
4
  "description": "Transform software testing into verifiable, signed, and traceable proofs",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.2",