qa360 2.2.12 → 2.2.13
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.js +20 -6
- package/cli/dist/core/pack/validator.js +2 -2
- package/cli/dist/core/pack-v2/migrator.d.ts +1 -0
- package/cli/dist/core/pack-v2/migrator.js +25 -0
- 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
|
*/
|
|
@@ -191,6 +191,20 @@ function generateFormTestSteps(form) {
|
|
|
191
191
|
}
|
|
192
192
|
return steps;
|
|
193
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
|
+
}
|
|
194
208
|
/**
|
|
195
209
|
* Generate YAML for a UI test definition
|
|
196
210
|
*/
|
|
@@ -198,20 +212,20 @@ function generateUiTestYaml(test) {
|
|
|
198
212
|
const steps = test.steps.map(step => {
|
|
199
213
|
let yaml = ` - action: ${step.action}`;
|
|
200
214
|
if (step.description)
|
|
201
|
-
yaml += `\n description: "${step.description}"`;
|
|
215
|
+
yaml += `\n description: "${escapeYamlString(step.description)}"`;
|
|
202
216
|
if (step.selector)
|
|
203
|
-
yaml += `\n selector: "${step.selector}"`;
|
|
217
|
+
yaml += `\n selector: "${escapeYamlString(step.selector)}"`;
|
|
204
218
|
if (step.value)
|
|
205
|
-
yaml += `\n value: "${step.value}"`;
|
|
219
|
+
yaml += `\n value: "${escapeYamlString(String(step.value))}"`;
|
|
206
220
|
if (step.expected)
|
|
207
221
|
yaml += `\n expected: ${JSON.stringify(step.expected)}`;
|
|
208
222
|
if (step.wait)
|
|
209
223
|
yaml += `\n wait: ${step.wait}`;
|
|
210
224
|
return yaml;
|
|
211
225
|
}).join('\n');
|
|
212
|
-
return ` - name: "${test.name}"
|
|
213
|
-
description: "${test.description || ''}"
|
|
214
|
-
path: "${test.path || '/'}"
|
|
226
|
+
return ` - name: "${escapeYamlString(test.name)}"
|
|
227
|
+
description: "${escapeYamlString(test.description || '')}"
|
|
228
|
+
path: "${escapeYamlString(test.path || '/')}"
|
|
215
229
|
steps:
|
|
216
230
|
${steps}`;
|
|
217
231
|
}
|
|
@@ -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)) {
|
|
@@ -424,8 +424,33 @@ export class PackMigrator {
|
|
|
424
424
|
}
|
|
425
425
|
/**
|
|
426
426
|
* Sanitize gate name for v2 format
|
|
427
|
+
* Handles both strings and objects (from YAML parsing edge cases)
|
|
427
428
|
*/
|
|
428
429
|
sanitizeGateName(gate) {
|
|
430
|
+
// Handle edge case where gate might be an object from YAML parsing
|
|
431
|
+
if (typeof gate !== 'string') {
|
|
432
|
+
// If gate is an object, try to extract a name
|
|
433
|
+
if (gate && typeof gate === 'object') {
|
|
434
|
+
// Try common property names
|
|
435
|
+
const possibleName = gate.name || gate.id || gate.gate;
|
|
436
|
+
if (typeof possibleName === 'string') {
|
|
437
|
+
gate = possibleName;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// Fallback: stringify the object keys
|
|
441
|
+
const keys = Object.keys(gate);
|
|
442
|
+
if (keys.length === 1) {
|
|
443
|
+
gate = keys[0];
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
throw new Error(`Invalid gate format: expected string, got object with keys: ${keys.join(', ')}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
throw new Error(`Invalid gate format: expected string, got ${typeof gate}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
429
454
|
// Map v1 gate names to v2 conventions
|
|
430
455
|
const nameMap = {
|
|
431
456
|
'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 {};
|