qa360 2.2.12 → 2.2.14
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/cli/dist/core/ai/ollama-provider.js +15 -3
- package/cli/dist/core/crawler/selector-generator.js +72 -3
- package/cli/dist/core/generation/crawler-pack-generator.d.ts +1 -1
- package/cli/dist/core/generation/crawler-pack-generator.js +31 -143
- package/cli/dist/core/pack/validator.js +2 -2
- package/cli/dist/core/pack-v2/migrator.d.ts +5 -0
- package/cli/dist/core/pack-v2/migrator.js +74 -6
- package/cli/dist/core/pack-v2/validator.js +4 -3
- package/cli/dist/core/runner/phase3-runner.js +12 -1
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/cli/dist/core/core/coverage/analyzer.d.ts +0 -101
- package/cli/dist/core/core/coverage/analyzer.js +0 -415
- package/cli/dist/core/core/coverage/collector.d.ts +0 -74
- package/cli/dist/core/core/coverage/collector.js +0 -459
- package/cli/dist/core/core/coverage/config.d.ts +0 -37
- package/cli/dist/core/core/coverage/config.js +0 -156
- package/cli/dist/core/core/coverage/index.d.ts +0 -11
- package/cli/dist/core/core/coverage/index.js +0 -15
- package/cli/dist/core/core/coverage/types.d.ts +0 -267
- package/cli/dist/core/core/coverage/types.js +0 -6
- package/cli/dist/core/core/coverage/vault.d.ts +0 -95
- package/cli/dist/core/core/coverage/vault.js +0 -405
|
@@ -32,16 +32,28 @@ export class OllamaProvider {
|
|
|
32
32
|
}
|
|
33
33
|
async isAvailable() {
|
|
34
34
|
try {
|
|
35
|
-
// Use timeout
|
|
35
|
+
// Use a longer timeout (15 seconds) to accommodate slower systems
|
|
36
|
+
// Ollama can take time to respond, especially on first run or with many models
|
|
36
37
|
const controller = new AbortController();
|
|
37
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
38
|
+
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
38
39
|
const response = await fetch(`${this.baseUrl}/api/tags`, {
|
|
39
40
|
signal: controller.signal,
|
|
40
41
|
});
|
|
41
42
|
clearTimeout(timeoutId);
|
|
42
43
|
return response.ok;
|
|
43
44
|
}
|
|
44
|
-
catch {
|
|
45
|
+
catch (error) {
|
|
46
|
+
// Log the error for debugging (will be visible in verbose mode)
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
if (error.name === 'AbortError') {
|
|
49
|
+
// Timeout - Ollama is slow to respond
|
|
50
|
+
console.debug(`[Ollama] Connection to ${this.baseUrl} timed out after 15s`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Other error (ECONNREFUSED, ENOTFOUND, etc.)
|
|
54
|
+
console.debug(`[Ollama] Connection error: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
return false;
|
|
46
58
|
}
|
|
47
59
|
}
|
|
@@ -50,9 +50,12 @@ export function generateSelector(element) {
|
|
|
50
50
|
if (className && !isGenericClass(className)) {
|
|
51
51
|
const classes = className.split(' ').filter(c => !isGenericClass(c));
|
|
52
52
|
if (classes.length > 0) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Smart class selection: prioritize meaningful classes
|
|
54
|
+
const meaningfulClasses = selectMeaningfulClasses(classes);
|
|
55
|
+
const classSelector = meaningfulClasses.map(c => `.${escapeCss(c)}`).join('');
|
|
56
|
+
// Limit total selector length
|
|
57
|
+
const qualifiedSelector = tagName ? `${tagName}${classSelector}` : classSelector;
|
|
58
|
+
return truncateSelector(qualifiedSelector, 200);
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
// 7. Tag + text content (for buttons, links)
|
|
@@ -169,6 +172,72 @@ function isSelectorStable(selector) {
|
|
|
169
172
|
function escapeCss(str) {
|
|
170
173
|
return str.replace(/(["\\])/g, '\\$1').replace(/"/g, '\\"');
|
|
171
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Select the most meaningful classes from a class list
|
|
177
|
+
* Prioritizes: unique identifiers, component names, action-oriented classes
|
|
178
|
+
* Deprioritizes: utility classes (spacing, colors, sizing)
|
|
179
|
+
*/
|
|
180
|
+
function selectMeaningfulClasses(classes) {
|
|
181
|
+
// Tailwind and other utility class patterns to deprioritize
|
|
182
|
+
const utilityPatterns = [
|
|
183
|
+
/^(p|m|px|py|pt|pb|pl|pr)-/, // spacing
|
|
184
|
+
/^(mt|mb|ml|mr)-/, // margin spacing
|
|
185
|
+
/^(text|bg|border)-/, // colors
|
|
186
|
+
/^(w|h)-/, // sizing
|
|
187
|
+
/^(flex|grid|block|inline)/, // display
|
|
188
|
+
/^(justify|items|self)/, // flexbox/grid
|
|
189
|
+
/^(rounded|shadow|opacity)/, // styling
|
|
190
|
+
/^gap-/, // flex gap
|
|
191
|
+
/^(line-)?height-/, // height
|
|
192
|
+
/^(font|weight|size)/, // typography
|
|
193
|
+
];
|
|
194
|
+
// Score each class by "meaningfulness"
|
|
195
|
+
const scored = classes.map(cls => {
|
|
196
|
+
let score = 50; // base score
|
|
197
|
+
// Bonus: component-like names (contain meaningful words)
|
|
198
|
+
if (/[A-Z][a-z]+/.test(cls))
|
|
199
|
+
score += 30; // CamelCase components
|
|
200
|
+
if (/button|btn|input|form|card|modal|nav|header|footer|sidebar|menu|dropdown/i.test(cls))
|
|
201
|
+
score += 25;
|
|
202
|
+
// Bonus: contains numbers (likely unique)
|
|
203
|
+
if (/\d+/.test(cls))
|
|
204
|
+
score += 15;
|
|
205
|
+
// Penalty: utility classes
|
|
206
|
+
for (const pattern of utilityPatterns) {
|
|
207
|
+
if (pattern.test(cls)) {
|
|
208
|
+
score -= 40;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { class: cls, score };
|
|
213
|
+
});
|
|
214
|
+
// Sort by score (descending) and take top 5
|
|
215
|
+
scored.sort((a, b) => b.score - a.score);
|
|
216
|
+
return scored.slice(0, 5).map(s => s.class);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Truncate selector to max length while keeping it valid
|
|
220
|
+
* Preserves the most specific parts
|
|
221
|
+
*/
|
|
222
|
+
function truncateSelector(selector, maxLength) {
|
|
223
|
+
if (selector.length <= maxLength)
|
|
224
|
+
return selector;
|
|
225
|
+
// For class-based selectors, remove classes from the middle
|
|
226
|
+
const classMatch = selector.match(/^(\w+)((\.[\w-]+)+)$/);
|
|
227
|
+
if (classMatch) {
|
|
228
|
+
const tag = classMatch[1];
|
|
229
|
+
const classes = classMatch[2].split('.').filter(c => c); // Remove empty strings
|
|
230
|
+
// Keep first 2 and last 2 classes (most specific)
|
|
231
|
+
if (classes.length > 4) {
|
|
232
|
+
const kept = [...classes.slice(0, 2), ...classes.slice(-2)];
|
|
233
|
+
const truncated = `${tag}.${kept.join('.')}`;
|
|
234
|
+
if (truncated.length <= maxLength)
|
|
235
|
+
return truncated;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Final fallback: just truncate (may not be valid CSS but prevents overflow)
|
|
239
|
+
return selector.substring(0, maxLength - 3) + '...';
|
|
240
|
+
}
|
|
172
241
|
/**
|
|
173
242
|
* Optimize selector for resiliency
|
|
174
243
|
*/
|
|
@@ -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,165 +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"
|
|
104
|
-
|
|
105
|
-
# Pages discovered by crawler
|
|
106
|
-
pages:
|
|
107
|
-
${siteMap.pages.map(p => ` - "${p.path}"`).join('\n')}
|
|
76
|
+
execution:
|
|
77
|
+
default_timeout: 30000
|
|
78
|
+
default_retries: 2
|
|
79
|
+
on_failure: continue
|
|
108
80
|
|
|
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
|
-
* Generate YAML for a UI test definition
|
|
196
|
-
*/
|
|
197
|
-
function generateUiTestYaml(test) {
|
|
198
|
-
const steps = test.steps.map(step => {
|
|
199
|
-
let yaml = ` - action: ${step.action}`;
|
|
200
|
-
if (step.description)
|
|
201
|
-
yaml += `\n description: "${step.description}"`;
|
|
202
|
-
if (step.selector)
|
|
203
|
-
yaml += `\n selector: "${step.selector}"`;
|
|
204
|
-
if (step.value)
|
|
205
|
-
yaml += `\n value: "${step.value}"`;
|
|
206
|
-
if (step.expected)
|
|
207
|
-
yaml += `\n expected: ${JSON.stringify(step.expected)}`;
|
|
208
|
-
if (step.wait)
|
|
209
|
-
yaml += `\n wait: ${step.wait}`;
|
|
210
|
-
return yaml;
|
|
211
|
-
}).join('\n');
|
|
212
|
-
return ` - name: "${test.name}"
|
|
213
|
-
description: "${test.description || ''}"
|
|
214
|
-
path: "${test.path || '/'}"
|
|
215
|
-
steps:
|
|
216
|
-
${steps}`;
|
|
217
|
-
}
|
|
218
106
|
/**
|
|
219
107
|
* Quick crawl - generates pack without detailed analysis
|
|
220
108
|
*/
|
|
@@ -22,8 +22,8 @@ export class PackValidator {
|
|
|
22
22
|
const __dirname = dirname(__filename);
|
|
23
23
|
// Check multiple paths to handle both development and bundled npm environments
|
|
24
24
|
// From cli/dist/core/pack/validator.js, we need to reach core/schemas/pack.schema.json
|
|
25
|
-
const path1 = join(__dirname, '../../schemas/pack.schema.json');
|
|
26
|
-
const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json');
|
|
25
|
+
const path1 = join(__dirname, '../../schemas/pack.schema.json'); // bundled (cli/dist/core/schemas/)
|
|
26
|
+
const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json'); // npm package (core/schemas/)
|
|
27
27
|
let schemaPath = existsSync(path1) ? path1 : path2;
|
|
28
28
|
// Final fallback: throw error if no schema found
|
|
29
29
|
if (!existsSync(schemaPath)) {
|
|
@@ -41,12 +41,17 @@ 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
|
*/
|
|
47
51
|
private findSecretInEnv;
|
|
48
52
|
/**
|
|
49
53
|
* Sanitize gate name for v2 format
|
|
54
|
+
* Handles both strings and objects (from YAML parsing edge cases)
|
|
50
55
|
*/
|
|
51
56
|
private sanitizeGateName;
|
|
52
57
|
/**
|
|
@@ -395,14 +395,57 @@ 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 both ui and a11y 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 - also needs pages and baseUrl
|
|
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
|
+
}
|
|
404
416
|
}
|
|
405
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Transform relative page paths to full URLs
|
|
420
|
+
*/
|
|
421
|
+
transformPagesToFullUrls(pages, baseUrl) {
|
|
422
|
+
if (!pages || pages.length === 0)
|
|
423
|
+
return [];
|
|
424
|
+
if (!baseUrl)
|
|
425
|
+
return pages;
|
|
426
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
427
|
+
return pages.map((page) => {
|
|
428
|
+
// Handle string pages (e.g., "/", "/products")
|
|
429
|
+
if (typeof page === 'string') {
|
|
430
|
+
if (page.startsWith('http')) {
|
|
431
|
+
return page; // Already a full URL
|
|
432
|
+
}
|
|
433
|
+
return `${base}${page.startsWith('/') ? '' : '/'}${page}`;
|
|
434
|
+
}
|
|
435
|
+
// Handle object pages (e.g., { url: "/", ... })
|
|
436
|
+
if (page && typeof page === 'object' && page.url) {
|
|
437
|
+
const url = page.url;
|
|
438
|
+
if (url.startsWith('http')) {
|
|
439
|
+
return page; // Already a full URL
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
...page,
|
|
443
|
+
url: `${base}${url.startsWith('/') ? '' : '/'}${url}`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return page;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
406
449
|
/**
|
|
407
450
|
* Find a secret in environment variables by trying common names
|
|
408
451
|
*/
|
|
@@ -424,8 +467,33 @@ export class PackMigrator {
|
|
|
424
467
|
}
|
|
425
468
|
/**
|
|
426
469
|
* Sanitize gate name for v2 format
|
|
470
|
+
* Handles both strings and objects (from YAML parsing edge cases)
|
|
427
471
|
*/
|
|
428
472
|
sanitizeGateName(gate) {
|
|
473
|
+
// Handle edge case where gate might be an object from YAML parsing
|
|
474
|
+
if (typeof gate !== 'string') {
|
|
475
|
+
// If gate is an object, try to extract a name
|
|
476
|
+
if (gate && typeof gate === 'object') {
|
|
477
|
+
// Try common property names
|
|
478
|
+
const possibleName = gate.name || gate.id || gate.gate;
|
|
479
|
+
if (typeof possibleName === 'string') {
|
|
480
|
+
gate = possibleName;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Fallback: stringify the object keys
|
|
484
|
+
const keys = Object.keys(gate);
|
|
485
|
+
if (keys.length === 1) {
|
|
486
|
+
gate = keys[0];
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
throw new Error(`Invalid gate format: expected string, got object with keys: ${keys.join(', ')}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
throw new Error(`Invalid gate format: expected string, got ${typeof gate}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
429
497
|
// Map v1 gate names to v2 conventions
|
|
430
498
|
const nameMap = {
|
|
431
499
|
'api_smoke': 'smoke',
|
|
@@ -370,7 +370,8 @@ export class PackValidatorV2 {
|
|
|
370
370
|
// Validate auth profile reference
|
|
371
371
|
if (gate.auth) {
|
|
372
372
|
// Will be validated against profiles in auth section
|
|
373
|
-
|
|
373
|
+
const adapter = typeof gate.adapter === 'string' ? gate.adapter : '';
|
|
374
|
+
if (adapter && !adapter.includes('api') && !adapter.includes('ui')) {
|
|
374
375
|
warnings.push({
|
|
375
376
|
code: 'QP2V019',
|
|
376
377
|
message: `Auth profile specified but adapter may not support auth`,
|
|
@@ -593,7 +594,7 @@ export class PackValidatorV2 {
|
|
|
593
594
|
return { errors, warnings };
|
|
594
595
|
}
|
|
595
596
|
// Check for API gate with api auth but no auth profile
|
|
596
|
-
const apiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter
|
|
597
|
+
const apiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('api'));
|
|
597
598
|
if (apiGates.length > 0 && !pack.auth?.api && !pack.auth?.profiles) {
|
|
598
599
|
warnings.push({
|
|
599
600
|
code: 'QP2V034',
|
|
@@ -603,7 +604,7 @@ export class PackValidatorV2 {
|
|
|
603
604
|
});
|
|
604
605
|
}
|
|
605
606
|
// Check for UI gate with ui auth but no auth profile
|
|
606
|
-
const uiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter
|
|
607
|
+
const uiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('ui'));
|
|
607
608
|
if (uiGates.length > 0 && !pack.auth?.ui && !pack.auth?.profiles) {
|
|
608
609
|
warnings.push({
|
|
609
610
|
code: 'QP2V035',
|
|
@@ -881,10 +881,21 @@ export class Phase3Runner {
|
|
|
881
881
|
* Run UI gate (includes basic accessibility)
|
|
882
882
|
*/
|
|
883
883
|
async runUiGate() {
|
|
884
|
-
|
|
884
|
+
let target = this.getTargetWeb();
|
|
885
885
|
if (!target) {
|
|
886
886
|
throw new Error('UI gate requires targets.web configuration or gate config with baseUrl');
|
|
887
887
|
}
|
|
888
|
+
// Transform relative pages to full URLs for v1 packs
|
|
889
|
+
if (target.pages && target.baseUrl && !this.isPackV2(this.pack)) {
|
|
890
|
+
const baseUrl = target.baseUrl.replace(/\/$/, '');
|
|
891
|
+
target.pages = target.pages.map((p) => {
|
|
892
|
+
const pageStr = typeof p === 'object' && p.url ? p.url : p;
|
|
893
|
+
if (typeof pageStr === 'string' && !pageStr.startsWith('http')) {
|
|
894
|
+
return `${baseUrl}${pageStr.startsWith('/') ? '' : '/'}${pageStr}`;
|
|
895
|
+
}
|
|
896
|
+
return p;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
888
899
|
const credentials = await this.getCredentialsForGate('ui');
|
|
889
900
|
const adapter = new PlaywrightUiAdapter();
|
|
890
901
|
return await adapter.runSmokeTests({
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage Analyzer
|
|
3
|
-
*
|
|
4
|
-
* Analyzes coverage data to provide insights, trends, and recommendations.
|
|
5
|
-
*/
|
|
6
|
-
import type { FileCoverage, CoverageMetrics, CoverageResult, CoverageTrend, CoverageGap, CoverageComparison, CoverageThreshold, CoverageType, CoverageReport } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Historical coverage data point
|
|
9
|
-
*/
|
|
10
|
-
interface HistoricalCoverage {
|
|
11
|
-
runId: string;
|
|
12
|
-
timestamp: number;
|
|
13
|
-
metrics: CoverageMetrics;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Coverage Analyzer class
|
|
17
|
-
*/
|
|
18
|
-
export declare class CoverageAnalyzer {
|
|
19
|
-
private history;
|
|
20
|
-
/**
|
|
21
|
-
* Analyze coverage and generate insights
|
|
22
|
-
*/
|
|
23
|
-
analyze(result: CoverageResult, threshold?: CoverageThreshold): CoverageReport;
|
|
24
|
-
/**
|
|
25
|
-
* Check if coverage meets thresholds
|
|
26
|
-
*/
|
|
27
|
-
checkThresholds(metrics: CoverageMetrics, threshold?: CoverageThreshold): boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Check if a single file meets thresholds
|
|
30
|
-
*/
|
|
31
|
-
checkFileThresholds(file: FileCoverage, threshold?: CoverageThreshold): boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Find coverage gaps
|
|
34
|
-
*/
|
|
35
|
-
findGaps(files: Record<string, FileCoverage>, threshold?: CoverageThreshold): CoverageGap[];
|
|
36
|
-
/**
|
|
37
|
-
* Calculate priority for covering a file
|
|
38
|
-
*/
|
|
39
|
-
private calculatePriority;
|
|
40
|
-
/**
|
|
41
|
-
* Estimate effort to cover a file
|
|
42
|
-
*/
|
|
43
|
-
private estimateEffort;
|
|
44
|
-
/**
|
|
45
|
-
* Generate test suggestions for a file
|
|
46
|
-
*/
|
|
47
|
-
private generateSuggestions;
|
|
48
|
-
/**
|
|
49
|
-
* Group consecutive numbers into ranges
|
|
50
|
-
*/
|
|
51
|
-
private groupConsecutiveNumbers;
|
|
52
|
-
/**
|
|
53
|
-
* Get top and bottom files by coverage
|
|
54
|
-
*/
|
|
55
|
-
getTopFiles(files: Record<string, FileCoverage>, limit?: number): Array<{
|
|
56
|
-
path: string;
|
|
57
|
-
coverage: number;
|
|
58
|
-
type: 'best' | 'worst';
|
|
59
|
-
}>;
|
|
60
|
-
/**
|
|
61
|
-
* Compare two coverage results
|
|
62
|
-
*/
|
|
63
|
-
compare(baseResult: CoverageResult, compareResult: CoverageResult): CoverageComparison;
|
|
64
|
-
/**
|
|
65
|
-
* Add historical coverage data
|
|
66
|
-
*/
|
|
67
|
-
addHistory(key: string, data: HistoricalCoverage): void;
|
|
68
|
-
/**
|
|
69
|
-
* Get coverage trends
|
|
70
|
-
*/
|
|
71
|
-
getTrends(key: string, type?: CoverageType, limit?: number): CoverageTrend[];
|
|
72
|
-
/**
|
|
73
|
-
* Calculate trend direction
|
|
74
|
-
*/
|
|
75
|
-
getTrendDirection(trends: CoverageTrend[]): 'improving' | 'stable' | 'declining';
|
|
76
|
-
/**
|
|
77
|
-
* Predict future coverage based on trends
|
|
78
|
-
*/
|
|
79
|
-
predictCoverage(key: string, type: CoverageType | undefined, targetCoverage: number): {
|
|
80
|
-
predictedReach: number | null;
|
|
81
|
-
projectedCoverage: number;
|
|
82
|
-
confidence: 'high' | 'medium' | 'low';
|
|
83
|
-
};
|
|
84
|
-
/**
|
|
85
|
-
* Generate coverage summary text
|
|
86
|
-
*/
|
|
87
|
-
generateSummary(metrics: CoverageMetrics): string;
|
|
88
|
-
/**
|
|
89
|
-
* Format coverage percentage with color indicator
|
|
90
|
-
*/
|
|
91
|
-
formatCoverage(percentage: number, threshold?: number): string;
|
|
92
|
-
/**
|
|
93
|
-
* Clear history
|
|
94
|
-
*/
|
|
95
|
-
clearHistory(key?: string): void;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Create a coverage analyzer
|
|
99
|
-
*/
|
|
100
|
-
export declare function createCoverageAnalyzer(): CoverageAnalyzer;
|
|
101
|
-
export {};
|