qa360 2.1.0 → 2.1.2
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/dist/commands/scan.d.ts +5 -0
- package/dist/commands/scan.js +155 -0
- package/dist/commands/secrets.d.ts +8 -2
- package/dist/commands/secrets.js +31 -19
- package/dist/core/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/core/coverage/analyzer.js +415 -0
- package/dist/core/core/coverage/collector.d.ts +74 -0
- package/dist/core/core/coverage/collector.js +459 -0
- package/dist/core/core/coverage/config.d.ts +37 -0
- package/dist/core/core/coverage/config.js +156 -0
- package/dist/core/core/coverage/index.d.ts +11 -0
- package/dist/core/core/coverage/index.js +15 -0
- package/dist/core/core/coverage/types.d.ts +267 -0
- package/dist/core/core/coverage/types.js +6 -0
- package/dist/core/core/coverage/vault.d.ts +95 -0
- package/dist/core/core/coverage/vault.js +405 -0
- package/dist/core/runner/phase3-runner.js +24 -3
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/json-reporter.d.ts +10 -0
- package/dist/generators/json-reporter.js +12 -0
- package/dist/generators/test-generator.d.ts +18 -0
- package/dist/generators/test-generator.js +78 -0
- package/dist/index.js +7 -2
- package/dist/scanners/dom-scanner.d.ts +52 -0
- package/dist/scanners/dom-scanner.js +296 -0
- package/dist/scanners/index.d.ts +4 -0
- package/dist/scanners/index.js +4 -0
- package/dist/types/scan.d.ts +68 -0
- package/dist/types/scan.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command - Discover UI elements from a web page
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { chromium } from '@playwright/test';
|
|
6
|
+
import { DOMScanner } from '../scanners/dom-scanner.js';
|
|
7
|
+
import { JSONReporter } from '../generators/json-reporter.js';
|
|
8
|
+
import { TestGenerator } from '../generators/test-generator.js';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { mkdirSync } from 'fs';
|
|
11
|
+
export const scanCommand = new Command('scan');
|
|
12
|
+
scanCommand
|
|
13
|
+
.description('Scan a web page for UI elements and generate test configurations')
|
|
14
|
+
.argument('<url>', 'URL of the page to scan')
|
|
15
|
+
.option('-o, --output <file>', 'Output file path (default: ".qa360/discovery/elements.json")')
|
|
16
|
+
.option('-i, --include <types>', 'Element types to include: buttons,links,forms,inputs,images,headings,all (default: "all")')
|
|
17
|
+
.option('-e, --exclude <selectors>', 'CSS selectors to exclude (comma-separated)')
|
|
18
|
+
.option('-s, --screenshot', 'Capture screenshots of elements (not yet implemented)')
|
|
19
|
+
.option('-a, --auto-generate-test', 'Auto-generate QA360 test file')
|
|
20
|
+
.option('-t, --timeout <ms>', 'Navigation timeout in milliseconds', '30000')
|
|
21
|
+
.option('--headed', 'Run in headed mode (show browser)')
|
|
22
|
+
.action(async (url, options) => {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
// Validate URL
|
|
25
|
+
try {
|
|
26
|
+
new URL(url);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error(`❌ Invalid URL: ${url}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Parse include types
|
|
33
|
+
const includeTypes = options.include || 'all';
|
|
34
|
+
const include = includeTypes
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((t) => t.trim().toLowerCase());
|
|
37
|
+
// Validate element types
|
|
38
|
+
const validTypes = ['buttons', 'links', 'forms', 'inputs', 'images', 'headings', 'all'];
|
|
39
|
+
for (const type of include) {
|
|
40
|
+
if (!validTypes.includes(type)) {
|
|
41
|
+
console.error(`❌ Invalid element type: ${type}`);
|
|
42
|
+
console.error(` Valid types: ${validTypes.join(', ')}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Parse exclude selectors
|
|
47
|
+
const exclude = options.exclude
|
|
48
|
+
? options.exclude.split(',').map((s) => s.trim())
|
|
49
|
+
: [];
|
|
50
|
+
const scanOptions = {
|
|
51
|
+
url,
|
|
52
|
+
output: options.output || '.qa360/discovery/elements.json',
|
|
53
|
+
include,
|
|
54
|
+
exclude,
|
|
55
|
+
screenshot: options.screenshot || false,
|
|
56
|
+
autoGenerateTest: options.autoGenerateTest || false,
|
|
57
|
+
timeout: parseInt(options.timeout, 10) || 30000,
|
|
58
|
+
headless: !options.headed
|
|
59
|
+
};
|
|
60
|
+
console.log(`🔍 Scanning page: ${url}`);
|
|
61
|
+
console.log('━'.repeat(50));
|
|
62
|
+
let browser = null;
|
|
63
|
+
let context = null;
|
|
64
|
+
let page = null;
|
|
65
|
+
try {
|
|
66
|
+
// Launch browser
|
|
67
|
+
browser = await chromium.launch({
|
|
68
|
+
headless: scanOptions.headless
|
|
69
|
+
});
|
|
70
|
+
context = await browser.newContext({
|
|
71
|
+
viewport: { width: 1280, height: 720 }
|
|
72
|
+
});
|
|
73
|
+
page = await context.newPage();
|
|
74
|
+
// Navigate to URL
|
|
75
|
+
await page.goto(url, {
|
|
76
|
+
timeout: scanOptions.timeout,
|
|
77
|
+
waitUntil: 'networkidle'
|
|
78
|
+
});
|
|
79
|
+
// Run scan
|
|
80
|
+
const scanner = new DOMScanner(page, url);
|
|
81
|
+
const elements = await scanner.scan(scanOptions);
|
|
82
|
+
// Generate summary
|
|
83
|
+
const summary = {
|
|
84
|
+
totalElements: elements.length,
|
|
85
|
+
buttons: elements.filter(e => e.type === 'button').length,
|
|
86
|
+
links: elements.filter(e => e.type === 'link').length,
|
|
87
|
+
forms: elements.filter(e => e.type === 'form').length,
|
|
88
|
+
inputs: elements.filter(e => e.type === 'input').length,
|
|
89
|
+
images: elements.filter(e => e.type === 'image').length,
|
|
90
|
+
headings: elements.filter(e => e.type === 'heading').length
|
|
91
|
+
};
|
|
92
|
+
// Print summary
|
|
93
|
+
console.log(`✅ Buttons found: ${summary.buttons}`);
|
|
94
|
+
console.log(`✅ Links found: ${summary.links}`);
|
|
95
|
+
console.log(`✅ Forms found: ${summary.forms}`);
|
|
96
|
+
console.log(`✅ Inputs found: ${summary.inputs}`);
|
|
97
|
+
console.log(`✅ Images found: ${summary.images}`);
|
|
98
|
+
console.log(`✅ Headings found: ${summary.headings}`);
|
|
99
|
+
console.log('━'.repeat(50));
|
|
100
|
+
console.log(`📦 Total elements: ${summary.totalElements}`);
|
|
101
|
+
console.log();
|
|
102
|
+
// Create output directory
|
|
103
|
+
const outputPath = resolve(scanOptions.output);
|
|
104
|
+
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
105
|
+
mkdirSync(outputDir, { recursive: true });
|
|
106
|
+
// Generate scan result
|
|
107
|
+
const result = {
|
|
108
|
+
scanDate: new Date().toISOString(),
|
|
109
|
+
url,
|
|
110
|
+
duration: Date.now() - startTime,
|
|
111
|
+
summary,
|
|
112
|
+
elements
|
|
113
|
+
};
|
|
114
|
+
// Generate JSON report
|
|
115
|
+
const reporter = new JSONReporter();
|
|
116
|
+
await reporter.generate(result, outputPath);
|
|
117
|
+
console.log(`💾 Saved to: ${outputPath}`);
|
|
118
|
+
// Auto-generate test if requested
|
|
119
|
+
if (options.autoGenerateTest) {
|
|
120
|
+
const testGenerator = new TestGenerator();
|
|
121
|
+
const testPath = outputPath.replace('.json', '.yml');
|
|
122
|
+
await testGenerator.generate(result, testPath);
|
|
123
|
+
console.log(`📄 Test generated: ${testPath}`);
|
|
124
|
+
}
|
|
125
|
+
// Screenshots (placeholder for future implementation)
|
|
126
|
+
if (options.screenshot) {
|
|
127
|
+
console.log(`📸 Screenshots: ${outputDir}/screenshots/ (not yet implemented)`);
|
|
128
|
+
}
|
|
129
|
+
// Cleanup
|
|
130
|
+
await page.close();
|
|
131
|
+
await context.close();
|
|
132
|
+
await browser.close();
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(`✨ Scan completed in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
135
|
+
console.log();
|
|
136
|
+
console.log('💡 Next steps:');
|
|
137
|
+
console.log(` • Review the discovered elements`);
|
|
138
|
+
if (options.autoGenerateTest) {
|
|
139
|
+
console.log(` • Run the auto-generated test: qa360 run ${outputPath.replace('.json', '.yml')}`);
|
|
140
|
+
}
|
|
141
|
+
console.log(` • Edit the test to remove unwanted elements`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// Cleanup on error
|
|
145
|
+
if (page)
|
|
146
|
+
await page.close().catch(() => { });
|
|
147
|
+
if (context)
|
|
148
|
+
await context.close().catch(() => { });
|
|
149
|
+
if (browser)
|
|
150
|
+
await browser.close().catch(() => { });
|
|
151
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
152
|
+
console.error(`❌ Scan failed: ${errorMessage}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
@@ -8,7 +8,10 @@ export declare class QA360Secrets {
|
|
|
8
8
|
/**
|
|
9
9
|
* Add a secret
|
|
10
10
|
*/
|
|
11
|
-
add(secretName?: string
|
|
11
|
+
add(secretName?: string, options?: {
|
|
12
|
+
value?: string;
|
|
13
|
+
nonInteractive?: boolean;
|
|
14
|
+
}): Promise<{
|
|
12
15
|
success: boolean;
|
|
13
16
|
exitCode: number;
|
|
14
17
|
}>;
|
|
@@ -43,7 +46,10 @@ export declare class QA360Secrets {
|
|
|
43
46
|
exitCode: number;
|
|
44
47
|
}>;
|
|
45
48
|
}
|
|
46
|
-
export declare function secretsAddCommand(secretName?: string
|
|
49
|
+
export declare function secretsAddCommand(secretName?: string, options?: {
|
|
50
|
+
value?: string;
|
|
51
|
+
nonInteractive?: boolean;
|
|
52
|
+
}): Promise<void>;
|
|
47
53
|
export declare function secretsListCommand(): Promise<void>;
|
|
48
54
|
export declare function secretsRemoveCommand(secretName?: string): Promise<void>;
|
|
49
55
|
export declare function secretsDoctorCommand(): Promise<void>;
|
package/dist/commands/secrets.js
CHANGED
|
@@ -14,12 +14,17 @@ export class QA360Secrets {
|
|
|
14
14
|
/**
|
|
15
15
|
* Add a secret
|
|
16
16
|
*/
|
|
17
|
-
async add(secretName) {
|
|
17
|
+
async add(secretName, options = {}) {
|
|
18
18
|
try {
|
|
19
19
|
let name = secretName;
|
|
20
20
|
let value;
|
|
21
|
-
// Interactive mode if no name provided
|
|
21
|
+
// Interactive mode if no name provided (and not non-interactive)
|
|
22
22
|
if (!name) {
|
|
23
|
+
if (options.nonInteractive) {
|
|
24
|
+
console.log(chalk.red('❌ Nom du secret requis en mode non-interactif'));
|
|
25
|
+
console.log(chalk.yellow('💡 Usage: qa360 secrets add <NOM> --value <VALUE> --non-interactive'));
|
|
26
|
+
return { success: false, exitCode: 1 };
|
|
27
|
+
}
|
|
23
28
|
const nameAnswer = await inquirer.prompt([{
|
|
24
29
|
type: 'input',
|
|
25
30
|
name: 'secretName',
|
|
@@ -39,22 +44,29 @@ export class QA360Secrets {
|
|
|
39
44
|
console.log(chalk.yellow('💡 Format requis: MAJUSCULES_AVEC_UNDERSCORES (ex: API_TOKEN)'));
|
|
40
45
|
return { success: false, exitCode: 1 };
|
|
41
46
|
}
|
|
42
|
-
// Get secret value
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Get secret value
|
|
48
|
+
if (options.value) {
|
|
49
|
+
// Non-interactive mode: value provided via option
|
|
50
|
+
value = options.value;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Interactive mode: prompt for value
|
|
54
|
+
const valueAnswer = await inquirer.prompt([{
|
|
55
|
+
type: 'password',
|
|
56
|
+
name: 'secretValue',
|
|
57
|
+
message: `Valeur pour ${name}:`,
|
|
58
|
+
mask: '*',
|
|
59
|
+
validate: (input) => {
|
|
60
|
+
if (!input.trim()) {
|
|
61
|
+
return 'La valeur ne peut pas être vide';
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
51
64
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!SecretsCrypto.looksLikeSecret(value)) {
|
|
65
|
+
}]);
|
|
66
|
+
value = valueAnswer.secretValue;
|
|
67
|
+
}
|
|
68
|
+
// Check if it looks like a secret (skip in non-interactive mode)
|
|
69
|
+
if (!options.nonInteractive && !SecretsCrypto.looksLikeSecret(value)) {
|
|
58
70
|
const confirmAnswer = await inquirer.prompt([{
|
|
59
71
|
type: 'confirm',
|
|
60
72
|
name: 'confirm',
|
|
@@ -250,9 +262,9 @@ export class QA360Secrets {
|
|
|
250
262
|
}
|
|
251
263
|
}
|
|
252
264
|
// Command handlers
|
|
253
|
-
export async function secretsAddCommand(secretName) {
|
|
265
|
+
export async function secretsAddCommand(secretName, options) {
|
|
254
266
|
const secrets = new QA360Secrets();
|
|
255
|
-
const result = await secrets.add(secretName);
|
|
267
|
+
const result = await secrets.add(secretName, options);
|
|
256
268
|
process.exit(result.exitCode);
|
|
257
269
|
}
|
|
258
270
|
export async function secretsListCommand() {
|
|
@@ -0,0 +1,101 @@
|
|
|
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 {};
|