create-backlist 10.0.2 → 10.0.4
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/bin/qa.js +138 -183
- package/package.json +6 -1
- package/src/qa/analyzers/accessibility.js +81 -0
- package/src/qa/analyzers/api.js +125 -0
- package/src/qa/analyzers/performance.js +137 -0
- package/src/qa/analyzers/security.js +207 -0
- package/src/qa/analyzers/seo.js +248 -0
- package/src/qa/browser/crawler.js +223 -0
- package/src/qa/browser/interactions.js +317 -0
- package/src/qa/browser/screenshot.js +34 -0
- package/src/qa/qa-engine.js +748 -1286
- package/src/qa/reporters/html.js +623 -0
- package/src/qa/reporters/json.js +49 -0
- package/src/qa/reporters/terminal.js +184 -0
- package/src/qa/utils/ai-classifier.js +98 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// CI/CD JSON reporter — structured output for integrations
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { VERSION } from '../qa-engine.js';
|
|
5
|
+
|
|
6
|
+
export class JSONReporter {
|
|
7
|
+
#session;
|
|
8
|
+
|
|
9
|
+
constructor(session) { this.#session = session; }
|
|
10
|
+
|
|
11
|
+
async generate(reportDir) {
|
|
12
|
+
const summary = this.#session.getSummary();
|
|
13
|
+
const filename = `${this.#session.id.toLowerCase()}.json`;
|
|
14
|
+
const filepath = path.join(reportDir, filename);
|
|
15
|
+
|
|
16
|
+
const output = {
|
|
17
|
+
meta: {
|
|
18
|
+
version : VERSION,
|
|
19
|
+
runId : this.#session.id,
|
|
20
|
+
generatedAt: new Date().toISOString(),
|
|
21
|
+
dataSource : 'real-runtime-testing',
|
|
22
|
+
},
|
|
23
|
+
urls : this.#session.urls,
|
|
24
|
+
summary,
|
|
25
|
+
results : this.#session.results,
|
|
26
|
+
bugs : this.#session.bugs,
|
|
27
|
+
routeMap : this.#session.routeMap,
|
|
28
|
+
apiLog : this.#session.apiLog,
|
|
29
|
+
secFindings: this.#session.secFindings,
|
|
30
|
+
perfMetrics: this.#session.perfMetrics,
|
|
31
|
+
a11yResults: this.#session.a11yResults,
|
|
32
|
+
seoResults : this.#session.seoResults,
|
|
33
|
+
consoleErrors: this.#session.consoleErrors,
|
|
34
|
+
networkLog : this.#session.networkLog.filter(e => e.type === 'failed'),
|
|
35
|
+
screenshots: this.#session.screenshots,
|
|
36
|
+
// CI-friendly exit signals
|
|
37
|
+
ci: {
|
|
38
|
+
exitCode : summary.failed > 0 || this.#session.bugs.some(b => b.severity === 'P0') ? 1 : 0,
|
|
39
|
+
p0BugCount : this.#session.bugs.filter(b => b.severity === 'P0').length,
|
|
40
|
+
p1BugCount : this.#session.bugs.filter(b => b.severity === 'P1').length,
|
|
41
|
+
passRate : summary.passRate,
|
|
42
|
+
passed : summary.passed === summary.total,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
await fs.writeJson(filepath, output, { spaces: 2 });
|
|
47
|
+
return filepath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Real-time terminal dashboard — shows ACTUAL test execution
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { formatDuration } from '../qa-engine.js';
|
|
4
|
+
|
|
5
|
+
const ESC = '\x1b[';
|
|
6
|
+
const CLEAR_LINE = ESC + '2K\r';
|
|
7
|
+
const CURSOR_UP = (n) => ESC + `${n}A`;
|
|
8
|
+
const CURSOR_HIDE = ESC + '?25l';
|
|
9
|
+
const CURSOR_SHOW = ESC + '?25h';
|
|
10
|
+
|
|
11
|
+
export class TerminalDashboard {
|
|
12
|
+
#session;
|
|
13
|
+
#lines = 0;
|
|
14
|
+
#active = false;
|
|
15
|
+
#startTime = Date.now();
|
|
16
|
+
#phase = 'Initializing...';
|
|
17
|
+
#currentTest = '';
|
|
18
|
+
#log = [];
|
|
19
|
+
#timer = null;
|
|
20
|
+
|
|
21
|
+
constructor(session) { this.#session = session; }
|
|
22
|
+
|
|
23
|
+
start() {
|
|
24
|
+
this.#active = true;
|
|
25
|
+
this.#startTime = Date.now();
|
|
26
|
+
process.stdout.write(CURSOR_HIDE);
|
|
27
|
+
this.#render();
|
|
28
|
+
this.#timer = setInterval(() => this.#render(), 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
stop() {
|
|
32
|
+
this.#active = false;
|
|
33
|
+
if (this.#timer) { clearInterval(this.#timer); this.#timer = null; }
|
|
34
|
+
process.stdout.write(CURSOR_SHOW);
|
|
35
|
+
this.#clear();
|
|
36
|
+
this.#printFinalSummary();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setPhase(phase) { this.#phase = phase; this.log(chalk.cyan(phase)); }
|
|
40
|
+
setCurrentTest(name) { this.#currentTest = name; }
|
|
41
|
+
addResult(result) { this.#currentTest = ''; }
|
|
42
|
+
|
|
43
|
+
log(msg) {
|
|
44
|
+
const entry = `${chalk.gray(new Date().toLocaleTimeString())} ${msg}`;
|
|
45
|
+
this.#log.push(entry);
|
|
46
|
+
if (this.#log.length > 12) this.#log.shift();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#render() {
|
|
50
|
+
if (!this.#active) return;
|
|
51
|
+
this.#clear();
|
|
52
|
+
|
|
53
|
+
const lines = this.#buildLines();
|
|
54
|
+
this.#lines = lines.length;
|
|
55
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#clear() {
|
|
59
|
+
if (this.#lines > 0) {
|
|
60
|
+
process.stdout.write(CURSOR_UP(this.#lines) + CLEAR_LINE);
|
|
61
|
+
for (let i = 1; i < this.#lines; i++) {
|
|
62
|
+
process.stdout.write('\n' + CLEAR_LINE);
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(CURSOR_UP(this.#lines - 1));
|
|
65
|
+
}
|
|
66
|
+
this.#lines = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#buildLines() {
|
|
70
|
+
const elapsed = ((Date.now() - this.#startTime) / 1000).toFixed(1);
|
|
71
|
+
const results = this.#session.results;
|
|
72
|
+
const bugs = this.#session.bugs;
|
|
73
|
+
const passed = results.filter(r => r.status === 'PASS' || r.status === 'FLAKY').length;
|
|
74
|
+
const failed = results.filter(r => r.status === 'FAIL').length;
|
|
75
|
+
const total = results.length;
|
|
76
|
+
const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
|
77
|
+
const mem = process.memoryUsage();
|
|
78
|
+
const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
|
|
79
|
+
|
|
80
|
+
const w = Math.min(process.stdout.columns || 80, 90);
|
|
81
|
+
const bar = '─'.repeat(w - 2);
|
|
82
|
+
const lines = [];
|
|
83
|
+
|
|
84
|
+
const c1 = chalk.hex('#00F5FF');
|
|
85
|
+
const c2 = chalk.hex('#BF40FF');
|
|
86
|
+
const pad = (s) => String(s).padEnd(w - 2);
|
|
87
|
+
|
|
88
|
+
lines.push(c1(`┌${bar}┐`));
|
|
89
|
+
lines.push(c1('│') + c2.bold(pad(` ⚡ BACKLIST ENTERPRISE QA v12.0 — REAL RUNTIME TESTING`)) + c1('│'));
|
|
90
|
+
lines.push(c1(`├${bar}┤`));
|
|
91
|
+
lines.push(c1('│') + pad(` ${chalk.cyan('Phase:')} ${chalk.white(this.#phase.slice(0, w - 14))}`) + c1('│'));
|
|
92
|
+
lines.push(c1(`├${bar}┤`));
|
|
93
|
+
|
|
94
|
+
// Real metrics
|
|
95
|
+
const pBar = this.#progressBar(passRate, 28);
|
|
96
|
+
lines.push(c1('│') + pad(
|
|
97
|
+
` ${chalk.green('✓')} ${chalk.bold(passed)} pass ` +
|
|
98
|
+
`${chalk.red('✗')} ${chalk.bold(failed)} fail ` +
|
|
99
|
+
`${chalk.cyan('🐛')} ${chalk.bold(bugs.length)} bugs ` +
|
|
100
|
+
`${chalk.gray('⏱')} ${chalk.white(elapsed + 's')} ` +
|
|
101
|
+
`${chalk.gray('Heap')} ${chalk.white(heapMB + 'MB')}`
|
|
102
|
+
) + c1('│'));
|
|
103
|
+
lines.push(c1('│') + pad(
|
|
104
|
+
` [${pBar}] ${chalk.bold(passRate + '%')} (${total} real tests)`
|
|
105
|
+
) + c1('│'));
|
|
106
|
+
lines.push(c1(`├${bar}┤`));
|
|
107
|
+
|
|
108
|
+
// Current test
|
|
109
|
+
const ct = this.#currentTest
|
|
110
|
+
? ` ${chalk.yellow('⟳')} ${chalk.yellow('Testing:')} ${chalk.white(this.#currentTest.slice(0, w - 18))}`
|
|
111
|
+
: ` ${chalk.gray('⊙ Awaiting next test...')}`;
|
|
112
|
+
lines.push(c1('│') + pad(ct) + c1('│'));
|
|
113
|
+
lines.push(c1(`├${bar}┤`));
|
|
114
|
+
|
|
115
|
+
// Discovered routes
|
|
116
|
+
const routeCount = this.#session.routeMap.length;
|
|
117
|
+
const apiCount = this.#session.apiLog.length;
|
|
118
|
+
const screensCount = this.#session.screenshots.length;
|
|
119
|
+
lines.push(c1('│') + pad(
|
|
120
|
+
` ${chalk.cyan('Routes:')} ${chalk.white(routeCount)} ` +
|
|
121
|
+
`${chalk.cyan('APIs:')} ${chalk.white(apiCount)} ` +
|
|
122
|
+
`${chalk.cyan('Screenshots:')} ${chalk.white(screensCount)} ` +
|
|
123
|
+
`${chalk.cyan('Net errors:')} ${chalk.white(this.#session.networkLog.filter(e => e.type === 'failed').length)}`
|
|
124
|
+
) + c1('│'));
|
|
125
|
+
lines.push(c1(`├${bar}┤`));
|
|
126
|
+
|
|
127
|
+
// Recent real results
|
|
128
|
+
lines.push(c1('│') + chalk.gray(pad(' Real Test Results:')) + c1('│'));
|
|
129
|
+
const recent = results.slice(-6);
|
|
130
|
+
for (const r of recent) {
|
|
131
|
+
const icon = r.status === 'PASS' ? chalk.green('✓')
|
|
132
|
+
: r.status === 'FAIL' ? chalk.red('✗')
|
|
133
|
+
: r.status === 'FLAKY' ? chalk.yellow('⚠')
|
|
134
|
+
: chalk.gray('⊘');
|
|
135
|
+
const dur = chalk.gray(formatDuration(r.duration || 0));
|
|
136
|
+
const name = r.name?.slice(0, w - 36) || '';
|
|
137
|
+
lines.push(c1('│') + pad(` ${icon} ${chalk.gray('[' + (r.type || '').padEnd(12) + ']')} ${chalk.white(name)} ${dur}`) + c1('│'));
|
|
138
|
+
}
|
|
139
|
+
for (let i = recent.length; i < 6; i++) {
|
|
140
|
+
lines.push(c1('│') + pad('') + c1('│'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Log
|
|
144
|
+
lines.push(c1(`├${bar}┤`));
|
|
145
|
+
lines.push(c1('│') + chalk.gray(pad(' Live log:')) + c1('│'));
|
|
146
|
+
for (const entry of this.#log.slice(-5)) {
|
|
147
|
+
lines.push(c1('│') + (' ' + entry).slice(0, w - 2).padEnd(w - 2) + c1('│'));
|
|
148
|
+
}
|
|
149
|
+
for (let i = this.#log.length; i < 5; i++) {
|
|
150
|
+
lines.push(c1('│') + pad('') + c1('│'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lines.push(c1(`└${bar}┘`));
|
|
154
|
+
lines.push(chalk.dim(` Real runtime data only · ${total} tests · ${bugs.length} bugs · Ctrl+C to stop`));
|
|
155
|
+
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#progressBar(pct, width = 20) {
|
|
160
|
+
const filled = Math.min(Math.round((pct / 100) * width), width);
|
|
161
|
+
const empty = width - filled;
|
|
162
|
+
const color = pct >= 90 ? chalk.green : pct >= 70 ? chalk.yellow : chalk.red;
|
|
163
|
+
return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#printFinalSummary() {
|
|
167
|
+
const summary = this.#session.getSummary();
|
|
168
|
+
const color = Number(summary.passRate) >= 90 ? chalk.green
|
|
169
|
+
: Number(summary.passRate) >= 70 ? chalk.yellow : chalk.red;
|
|
170
|
+
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(chalk.hex('#00F5FF').bold(' ── Real Runtime QA Results ─────────────────────────────'));
|
|
173
|
+
console.log(` Tests run: ${chalk.white.bold(summary.total)}`);
|
|
174
|
+
console.log(` Passed: ${chalk.green.bold(summary.passed)}`);
|
|
175
|
+
console.log(` Failed: ${chalk.red.bold(summary.failed)}`);
|
|
176
|
+
console.log(` Flaky: ${chalk.yellow.bold(summary.flaky)}`);
|
|
177
|
+
console.log(` Pass rate: ${color.bold(summary.passRate + '%')}`);
|
|
178
|
+
console.log(` Bugs found: ${chalk.cyan.bold(this.#session.bugs.length)}`);
|
|
179
|
+
console.log(` Duration: ${chalk.white(formatDuration(summary.duration))}`);
|
|
180
|
+
console.log(` Routes: ${chalk.white(this.#session.routeMap.length)} discovered`);
|
|
181
|
+
console.log(` Screenshots:${chalk.white(this.#session.screenshots.length)} captured`);
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// AI-assisted bug classification — pattern matching + heuristics
|
|
2
|
+
// (Works without external API — pure local intelligence)
|
|
3
|
+
|
|
4
|
+
const SEVERITY_PATTERNS = {
|
|
5
|
+
P0: [
|
|
6
|
+
/security|auth.*bypass|sql.inject|xss|rce|remote.code|exposed.*secret|password.*leak/i,
|
|
7
|
+
/crash|fatal|500.*error|server.*down|database.*connect.*fail/i,
|
|
8
|
+
/payment|transaction|data.*loss|corrupt/i,
|
|
9
|
+
],
|
|
10
|
+
P1: [
|
|
11
|
+
/login.*fail|auth.*error|jwt|token.*invalid/i,
|
|
12
|
+
/api.*timeout|endpoint.*down|cors.*error/i,
|
|
13
|
+
/form.*broken|submit.*fail|validation.*miss/i,
|
|
14
|
+
/lcp|largest.*contentful|performance.*poor/i,
|
|
15
|
+
/wcag|accessibility.*critical|a11y.*serious/i,
|
|
16
|
+
],
|
|
17
|
+
P2: [
|
|
18
|
+
/console.*error|js.*error|runtime.*error/i,
|
|
19
|
+
/network.*fail|fetch.*error|404/i,
|
|
20
|
+
/missing.*meta|seo.*issue|canonical/i,
|
|
21
|
+
/slow.*resource|image.*size|bundle.*large/i,
|
|
22
|
+
],
|
|
23
|
+
P3: [
|
|
24
|
+
/warning|minor|style|layout|cosmetic/i,
|
|
25
|
+
/todo|typo|label|placeholder/i,
|
|
26
|
+
/p3|low.*priority/i,
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CATEGORY_PATTERNS = {
|
|
31
|
+
'security' : /security|csp|hsts|cors|xss|injection|auth|token|csrf/i,
|
|
32
|
+
'performance' : /lcp|fcp|cls|fid|ttfb|tbt|slow|timeout|load.*time|render/i,
|
|
33
|
+
'accessibility': /wcag|a11y|aria|alt.*text|contrast|keyboard|screen.*reader/i,
|
|
34
|
+
'seo' : /title|meta|description|canonical|sitemap|robots|og:|h1/i,
|
|
35
|
+
'api' : /api|endpoint|status.*code|response|json|rest|graphql/i,
|
|
36
|
+
'javascript' : /js.*error|console.*error|uncaught|undefined|null.*reference/i,
|
|
37
|
+
'network' : /network|fetch|xhr|request.*fail|connection/i,
|
|
38
|
+
'form' : /form|input|submit|validation|field|required/i,
|
|
39
|
+
'auth' : /login|auth|token|session|password|credential|logout/i,
|
|
40
|
+
'ui' : /layout|render|visual|style|component|button|link/i,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const RECOMMENDATIONS = {
|
|
44
|
+
'security' : 'Review security configuration and conduct a penetration test',
|
|
45
|
+
'performance' : 'Run Lighthouse audit and optimize assets/server response times',
|
|
46
|
+
'accessibility': 'Fix WCAG 2.1 AA violations — use aXe DevTools for details',
|
|
47
|
+
'seo' : 'Fix meta tags and submit updated sitemap to Search Console',
|
|
48
|
+
'api' : 'Check API contract, add proper error handling and status codes',
|
|
49
|
+
'javascript' : 'Debug JavaScript errors in browser DevTools — add error boundaries',
|
|
50
|
+
'network' : 'Check CDN, server logs, and network configuration',
|
|
51
|
+
'form' : 'Add client-side and server-side validation to all forms',
|
|
52
|
+
'auth' : 'Audit authentication flow — check JWT expiry and refresh logic',
|
|
53
|
+
'ui' : 'Test across browsers and screen sizes — check CSS specificity',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export class AIClassifier {
|
|
57
|
+
async classify(bug, session) {
|
|
58
|
+
const text = `${bug.title} ${bug.description || ''}`.toLowerCase();
|
|
59
|
+
|
|
60
|
+
// Determine severity from patterns
|
|
61
|
+
let severity = bug.severity || 'P3';
|
|
62
|
+
let confidence = 0.7;
|
|
63
|
+
|
|
64
|
+
for (const [sev, patterns] of Object.entries(SEVERITY_PATTERNS)) {
|
|
65
|
+
if (patterns.some(p => p.test(text))) {
|
|
66
|
+
severity = sev;
|
|
67
|
+
confidence = 0.85;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine category
|
|
73
|
+
let category = bug.type || 'general';
|
|
74
|
+
for (const [cat, pattern] of Object.entries(CATEGORY_PATTERNS)) {
|
|
75
|
+
if (pattern.test(text)) {
|
|
76
|
+
category = cat;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Context from session
|
|
82
|
+
const relatedBugs = session.bugs.filter(b =>
|
|
83
|
+
b.id !== bug.id && b.type === bug.type
|
|
84
|
+
).length;
|
|
85
|
+
|
|
86
|
+
if (relatedBugs > 3) {
|
|
87
|
+
// Systemic issue — upgrade severity
|
|
88
|
+
const order = ['P3','P2','P1','P0'];
|
|
89
|
+
const idx = order.indexOf(severity);
|
|
90
|
+
if (idx > 0) { severity = order[idx - 1]; confidence = 0.9; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const recommendation = RECOMMENDATIONS[category]
|
|
94
|
+
|| 'Investigate and fix based on error details';
|
|
95
|
+
|
|
96
|
+
return { severity, category, recommendation, confidence };
|
|
97
|
+
}
|
|
98
|
+
}
|