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
package/bin/qa.js
CHANGED
|
@@ -1,180 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// Backlist QA CLI — qa.js v10.0
|
|
5
|
-
// Standalone QA entry point: `node bin/qa.js [mode] [--flags]`
|
|
6
|
-
// Copyright (c) W.A.H.ISHAN — MIT License
|
|
7
|
-
//
|
|
8
|
-
// NEW in v10.0:
|
|
9
|
-
// ✦ --url=<localhost> --prod=<production> flags for HTTP-based QA
|
|
10
|
-
// ✦ URL QA mode — probe real running URLs (no browser required)
|
|
11
|
-
// ✦ HTTP security header scanner, SEO checker, a11y basics
|
|
12
|
-
// ✦ Route crawler & benchmark per endpoint
|
|
13
|
-
// ✦ Dual-URL diff report (localhost vs production)
|
|
14
|
-
// ✦ All v9.0 features retained (manual, auto, history, post-gen)
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
2
|
+
// ── Backlist Enterprise QA CLI — qa.js v12.0 ─────────────────────────────
|
|
17
3
|
import * as p from '@clack/prompts';
|
|
18
4
|
import chalk from 'chalk';
|
|
19
5
|
import fs from 'fs-extra';
|
|
20
6
|
import path from 'node:path';
|
|
7
|
+
import { URL } from 'node:url';
|
|
21
8
|
import {
|
|
22
|
-
runManualQA,
|
|
23
9
|
runAutomatedQA,
|
|
24
10
|
runUrlQA,
|
|
11
|
+
runManualQA,
|
|
25
12
|
viewQAHistory,
|
|
26
13
|
initQASystem,
|
|
27
14
|
autoRunPostGeneration,
|
|
15
|
+
VERSION,
|
|
28
16
|
} from '../src/qa/qa-engine.js';
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const posArgs = argv.filter(a => !a.startsWith('--'));
|
|
37
|
-
|
|
38
|
-
const isContinuous = flags.has('--continuous') || flags.has('--watch') || flags.has('-w');
|
|
39
|
-
const isCI = process.env.QA_CI === '1' || flags.has('--ci');
|
|
40
|
-
const noColor = flags.has('--no-color') || process.env.NO_COLOR;
|
|
41
|
-
const isPostGen = flags.has('--post-gen');
|
|
42
|
-
const scope = argv.find(a => a.startsWith('--scope='))?.split('=')[1] ?? 'all';
|
|
18
|
+
const VERSION_DISPLAY = `12.0.0`;
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
|
21
|
+
const posArgs = argv.filter(a => !a.startsWith('--'));
|
|
22
|
+
const isCI = process.env.QA_CI === '1' || flags.has('--ci');
|
|
23
|
+
const modeArg = posArgs[0]?.toLowerCase();
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
const localUrlFlag = argv.find(a => a.startsWith('--url='))?.split('=').slice(1).join('=') ?? '';
|
|
46
|
-
const prodUrlFlag = argv.find(a => a.startsWith('--prod='))?.split('=').slice(1).join('=') ?? '';
|
|
25
|
+
const getFlag = (name) => argv.find(a => a.startsWith(`--${name}=`))?.split('=').slice(1).join('=') ?? '';
|
|
47
26
|
|
|
48
|
-
if (noColor) chalk.level = 0;
|
|
49
|
-
|
|
50
|
-
// ── Graceful shutdown ─────────────────────────────────────────────────────
|
|
51
27
|
let shuttingDown = false;
|
|
28
|
+
process.on('SIGINT', () => { if (!shuttingDown) { shuttingDown = true; process.stdout.write('\x1b[?25h'); console.log(chalk.gray('\n Shutting down...')); process.exit(0); } });
|
|
29
|
+
process.on('SIGTERM', () => { if (!shuttingDown) { shuttingDown = true; process.exit(0); } });
|
|
52
30
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
shuttingDown = true;
|
|
57
|
-
process.stdout.write('\x1b[?25h');
|
|
58
|
-
console.log('\n' + chalk.gray(` ${signal} received — shutting down gracefully…`));
|
|
59
|
-
p.cancel('QA session ended.');
|
|
60
|
-
process.exit(0);
|
|
61
|
-
};
|
|
62
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
63
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Banner ────────────────────────────────────────────────────────────────
|
|
67
|
-
function printQABanner() {
|
|
68
|
-
if (isCI) return;
|
|
69
|
-
|
|
70
|
-
const c1 = chalk.hex('#00F5FF');
|
|
71
|
-
const c2 = chalk.hex('#BF40FF');
|
|
72
|
-
const c3 = chalk.hex('#FF6B6B');
|
|
73
|
-
const c4 = chalk.hex('#00FF9F');
|
|
74
|
-
const dim = chalk.gray;
|
|
75
|
-
|
|
31
|
+
function printBanner() {
|
|
32
|
+
const c1 = chalk.hex('#00F5FF');
|
|
33
|
+
const c2 = chalk.hex('#BF40FF');
|
|
76
34
|
console.log('');
|
|
77
|
-
console.log(c1('
|
|
78
|
-
console.log(c1(' ║') + c2.bold('
|
|
79
|
-
console.log(c1(' ║') +
|
|
80
|
-
console.log(c1('
|
|
81
|
-
console.log(c1(' ║') + c2.bold('/ /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ') + c1('║'));
|
|
82
|
-
console.log(c1(' ║') + c2.bold('/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ') + c1('║'));
|
|
83
|
-
console.log(c1(' ║') + ' ' + c1('║'));
|
|
84
|
-
console.log(c1(' ║') + c3.bold(` 🧪 Live QA Platform v${VERSION} — HTTP + File + E2E `) + c1('║'));
|
|
85
|
-
console.log(c1(' ║') + dim(' URL Probe · Security · SEO · A11y · Performance · CI ') + c1('║'));
|
|
86
|
-
console.log(c1(' ╚══════════════════════════════════════════════════════════════╝'));
|
|
35
|
+
console.log(c1(' ╔═══════════════════════════════════════════════════════════╗'));
|
|
36
|
+
console.log(c1(' ║') + c2.bold(' BACKLIST ENTERPRISE AI QA PLATFORM v12.0 ') + c1('║'));
|
|
37
|
+
console.log(c1(' ║') + chalk.gray(' Real Browser Testing · Zero Fake Data · AI Classifier ') + c1('║'));
|
|
38
|
+
console.log(c1(' ╚═══════════════════════════════════════════════════════════╝'));
|
|
87
39
|
console.log('');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
c4('◉ LIVE') + dim(' │ ') +
|
|
95
|
-
dim('Node ') + chalk.white(process.version) + dim(' │ ') +
|
|
96
|
-
dim('Heap ') + chalk.white(heapMB + 'MB') + dim(' │ ') +
|
|
97
|
-
dim('Scope ') + chalk.white(scope) + dim(' │ ') +
|
|
98
|
-
dim('Mode ') + chalk.white(isContinuous ? 'watch' : 'single-run') + dim(' │ ') +
|
|
99
|
-
dim('v') + chalk.white(VERSION)
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
if (localUrlFlag) console.log(dim(' URL → ') + chalk.white(localUrlFlag));
|
|
103
|
-
if (prodUrlFlag) console.log(dim(' Prod → ') + chalk.white(prodUrlFlag));
|
|
104
|
-
|
|
105
|
-
console.log(dim(' Flags: --url=<localhost> --prod=<production> --continuous (-w)'));
|
|
106
|
-
console.log(dim(' --ci --scope=<all|backend|frontend|security> --post-gen --no-color'));
|
|
107
|
-
console.log(dim(' CI: QA_CI=1 node bin/qa.js auto --url=http://localhost:3000'));
|
|
108
|
-
console.log(dim(' ─────────────────────────────────────────────────────────────'));
|
|
40
|
+
console.log(chalk.gray(
|
|
41
|
+
` Node ${process.version} · ` +
|
|
42
|
+
`Heap ${(process.memoryUsage().heapUsed/1024/1024).toFixed(0)}MB · ` +
|
|
43
|
+
`v${VERSION_DISPLAY}`
|
|
44
|
+
));
|
|
45
|
+
console.log(chalk.gray(' Powered by Playwright · axe-core · Real HTTP probes'));
|
|
109
46
|
console.log('');
|
|
110
47
|
}
|
|
111
48
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const maj = parseInt(process.version.replace('v','').split('.')[0]);
|
|
117
|
-
if (maj < 18) throw new Error(`Node.js ${process.version} — requires v18+`);
|
|
118
|
-
}},
|
|
119
|
-
{ name: 'QA directories', fn: async () => {
|
|
120
|
-
await fs.ensureDir(path.join(process.cwd(), '.backlist', 'qa', 'reports'));
|
|
121
|
-
}},
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
const spin = p.spinner();
|
|
125
|
-
spin.start('System health check…');
|
|
126
|
-
for (const check of checks) {
|
|
127
|
-
try { await check.fn(); }
|
|
128
|
-
catch (err) { spin.stop(chalk.red(`✗ ${check.name}: ${err.message}`)); process.exit(1); }
|
|
129
|
-
}
|
|
130
|
-
spin.stop(chalk.green(`✓ System healthy — QA v${VERSION} ready`));
|
|
49
|
+
function validateUrl(u) {
|
|
50
|
+
if (!u) return true; // optional
|
|
51
|
+
try { new URL(u); return true; }
|
|
52
|
+
catch { return false; }
|
|
131
53
|
}
|
|
132
54
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
55
|
+
async function collectUrls() {
|
|
56
|
+
console.log(chalk.hex('#00F5FF').bold('\n ── Target URLs ──────────────────────────────────────'));
|
|
57
|
+
console.log(chalk.gray(' Provide at least one URL. The QA engine will automatically'));
|
|
58
|
+
console.log(chalk.gray(' crawl routes, discover APIs, and test real interactions.\n'));
|
|
59
|
+
|
|
60
|
+
const localUrl = await p.text({
|
|
61
|
+
message : 'Localhost URL:',
|
|
62
|
+
placeholder: 'http://localhost:3000',
|
|
63
|
+
validate : (v) => {
|
|
64
|
+
if (!v?.trim()) return undefined; // optional
|
|
65
|
+
if (!validateUrl(v.trim())) return '❌ Invalid URL';
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (p.isCancel(localUrl)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
69
|
+
|
|
70
|
+
const stagingUrl = await p.text({
|
|
71
|
+
message : 'Staging URL (blank to skip):',
|
|
72
|
+
placeholder: 'https://staging.yoursite.com',
|
|
73
|
+
validate : (v) => {
|
|
74
|
+
if (!v?.trim()) return undefined;
|
|
75
|
+
if (!validateUrl(v.trim())) return '❌ Invalid URL';
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
if (p.isCancel(stagingUrl)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
79
|
+
|
|
80
|
+
const prodUrl = await p.text({
|
|
81
|
+
message : 'Production URL (blank to skip):',
|
|
82
|
+
placeholder: 'https://yoursite.com',
|
|
83
|
+
validate : (v) => {
|
|
84
|
+
if (!v?.trim()) return undefined;
|
|
85
|
+
if (!validateUrl(v.trim())) return '❌ Invalid URL';
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (p.isCancel(prodUrl)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
137
89
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
process.exit(
|
|
90
|
+
const urls = {
|
|
91
|
+
localUrl : String(localUrl || '').trim() || undefined,
|
|
92
|
+
stagingUrl: String(stagingUrl || '').trim() || undefined,
|
|
93
|
+
prodUrl : String(prodUrl || '').trim() || undefined,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (!urls.localUrl && !urls.stagingUrl && !urls.prodUrl) {
|
|
97
|
+
console.log(chalk.red(' ❌ At least one URL is required for real testing.'));
|
|
98
|
+
process.exit(1);
|
|
147
99
|
}
|
|
148
100
|
|
|
149
|
-
|
|
150
|
-
|
|
101
|
+
return urls;
|
|
102
|
+
}
|
|
151
103
|
|
|
104
|
+
async function main() {
|
|
105
|
+
printBanner();
|
|
106
|
+
await initQASystem();
|
|
107
|
+
|
|
108
|
+
// ── CI / flag mode ──────────────────────────────────────────────────
|
|
152
109
|
if (isCI || modeArg) {
|
|
153
|
-
const
|
|
154
|
-
|
|
110
|
+
const urls = {
|
|
111
|
+
localUrl : getFlag('url') || getFlag('local') || undefined,
|
|
112
|
+
stagingUrl: getFlag('staging') || undefined,
|
|
113
|
+
prodUrl : getFlag('prod') || undefined,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const mode = modeArg || 'auto';
|
|
155
117
|
|
|
156
118
|
switch (mode) {
|
|
157
|
-
case 'manual':
|
|
158
|
-
await runManualQA();
|
|
159
|
-
break;
|
|
160
|
-
case 'auto':
|
|
161
|
-
await runAutomatedQA({
|
|
162
|
-
continuous: isContinuous,
|
|
163
|
-
localUrl : localUrlFlag || undefined,
|
|
164
|
-
prodUrl : prodUrlFlag || undefined,
|
|
165
|
-
});
|
|
166
|
-
break;
|
|
167
119
|
case 'url':
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
prodUrl : prodUrlFlag || undefined,
|
|
176
|
-
});
|
|
120
|
+
case 'scan': {
|
|
121
|
+
const result = await runUrlQA(urls);
|
|
122
|
+
if (isCI) process.exit(result?.session.getSummary().failed > 0 ? 1 : 0);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'auto': {
|
|
126
|
+
await runAutomatedQA({ ...urls, continuous: flags.has('--continuous') || flags.has('-w') });
|
|
177
127
|
break;
|
|
128
|
+
}
|
|
178
129
|
case 'history':
|
|
179
130
|
await viewQAHistory();
|
|
180
131
|
break;
|
|
@@ -182,69 +133,73 @@ async function main() {
|
|
|
182
133
|
await autoRunPostGeneration();
|
|
183
134
|
break;
|
|
184
135
|
default:
|
|
185
|
-
console.error(chalk.red(`Unknown mode: ${mode}. Use:
|
|
136
|
+
console.error(chalk.red(`Unknown mode: ${mode}. Use: scan | auto | history | post-gen`));
|
|
186
137
|
process.exit(1);
|
|
187
138
|
}
|
|
188
|
-
|
|
189
|
-
// CI exit code: non-zero if any failures
|
|
190
|
-
if (isCI) process.exit(0);
|
|
191
139
|
return;
|
|
192
140
|
}
|
|
193
141
|
|
|
194
|
-
// ── Interactive mode
|
|
195
|
-
p.intro(chalk.hex('#00F5FF').bold(
|
|
142
|
+
// ── Interactive mode ────────────────────────────────────────────────
|
|
143
|
+
p.intro(chalk.hex('#00F5FF').bold(' Backlist Enterprise QA Platform v12.0 '));
|
|
196
144
|
|
|
197
145
|
const mode = await p.select({
|
|
198
146
|
message: 'Select QA mode:',
|
|
199
147
|
options: [
|
|
200
|
-
{ value: '
|
|
201
|
-
{ value: '
|
|
202
|
-
{ value: '
|
|
203
|
-
{ value: '
|
|
204
|
-
{ value: 'post-gen',
|
|
205
|
-
{ value: 'history',
|
|
148
|
+
{ value: 'full-scan', label: '🔬 Full Enterprise Scan', hint: 'Browser + API + Security + Perf + A11y + SEO — real data only' },
|
|
149
|
+
{ value: 'quick-scan', label: '⚡ Quick Scan', hint: 'Fast connectivity + security + SEO check' },
|
|
150
|
+
{ value: 'manual', label: '🧪 Manual QA', hint: 'Choose specific scan types interactively' },
|
|
151
|
+
{ value: 'continuous', label: '🔄 Continuous Monitoring', hint: 'Re-runs every 60s — Ctrl+C to stop' },
|
|
152
|
+
{ value: 'post-gen', label: '🚀 Post-Gen Validation', hint: 'Validate freshly generated backend' },
|
|
153
|
+
{ value: 'history', label: '📜 View History', hint: 'Browse past real QA runs' },
|
|
206
154
|
],
|
|
207
155
|
});
|
|
208
|
-
if (p.isCancel(mode)) { p.cancel('
|
|
156
|
+
if (p.isCancel(mode)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
157
|
+
|
|
158
|
+
if (mode === 'history') { await viewQAHistory(); return; }
|
|
159
|
+
if (mode === 'post-gen') { await autoRunPostGeneration(); return; }
|
|
160
|
+
|
|
161
|
+
// All scan modes need URLs
|
|
162
|
+
const urls = await collectUrls();
|
|
163
|
+
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.hex('#BF40FF').bold(' ── QA Configuration ───────────────────────────────'));
|
|
166
|
+
if (urls.localUrl) console.log(chalk.gray(` Localhost: ${urls.localUrl}`));
|
|
167
|
+
if (urls.stagingUrl) console.log(chalk.gray(` Staging: ${urls.stagingUrl}`));
|
|
168
|
+
if (urls.prodUrl) console.log(chalk.gray(` Production: ${urls.prodUrl}`));
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
const confirm = await p.confirm({
|
|
172
|
+
message : 'Start real browser-based QA scan?',
|
|
173
|
+
initialValue: true,
|
|
174
|
+
});
|
|
175
|
+
if (p.isCancel(confirm) || !confirm) { p.cancel('Aborted.'); process.exit(0); }
|
|
209
176
|
|
|
210
177
|
switch (mode) {
|
|
211
|
-
case '
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await runUrlQA({ localUrl: localUrl.trim() || undefined, prodUrl: prodUrl.trim() || undefined });
|
|
178
|
+
case 'full-scan':
|
|
179
|
+
case 'quick-scan': {
|
|
180
|
+
const result = await runUrlQA(urls);
|
|
181
|
+
if (result?.htmlPath) {
|
|
182
|
+
p.outro(chalk.hex('#00F5FF').bold('✓ Scan complete — real runtime data only'));
|
|
183
|
+
console.log(chalk.gray(` 📄 HTML Report: ${result.htmlPath}`));
|
|
184
|
+
console.log(chalk.gray(` 📋 JSON Report: ${result.jsonPath}`));
|
|
185
|
+
}
|
|
220
186
|
break;
|
|
221
187
|
}
|
|
222
188
|
case 'manual':
|
|
223
189
|
await runManualQA();
|
|
224
190
|
break;
|
|
225
|
-
case '
|
|
226
|
-
await runAutomatedQA({
|
|
227
|
-
break;
|
|
228
|
-
case 'live':
|
|
229
|
-
await runAutomatedQA({ continuous: true, localUrl: localUrlFlag || undefined, prodUrl: prodUrlFlag || undefined });
|
|
230
|
-
break;
|
|
231
|
-
case 'post-gen':
|
|
232
|
-
await autoRunPostGeneration();
|
|
191
|
+
case 'continuous':
|
|
192
|
+
await runAutomatedQA({ ...urls, continuous: true });
|
|
233
193
|
break;
|
|
234
|
-
case 'history':
|
|
235
|
-
await viewQAHistory();
|
|
236
|
-
p.outro(chalk.hex('#00F5FF').bold('History viewed.'));
|
|
237
|
-
break;
|
|
238
|
-
default:
|
|
239
|
-
p.cancel('Unknown mode.');
|
|
240
|
-
process.exit(1);
|
|
241
194
|
}
|
|
242
195
|
}
|
|
243
196
|
|
|
244
197
|
main().catch(err => {
|
|
245
198
|
if (shuttingDown) return;
|
|
246
199
|
process.stdout.write('\x1b[?25h');
|
|
247
|
-
console.error(chalk.red.bold(`\n
|
|
248
|
-
if (err.stack && !isCI)
|
|
200
|
+
console.error(chalk.red.bold(`\n Fatal: ${err.message || err}`));
|
|
201
|
+
if (err.stack && !isCI) {
|
|
202
|
+
console.error(chalk.gray(err.stack.split('\n').filter(l => !l.includes('node_modules')).slice(0, 5).join('\n')));
|
|
203
|
+
}
|
|
249
204
|
process.exit(1);
|
|
250
205
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-backlist",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.4",
|
|
4
4
|
"description": "An advanced, multi-language backend generator based on frontend analysis. Smart Freemium SaaS CLI with Live QA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
"src",
|
|
13
13
|
"AiModuls"
|
|
14
14
|
],
|
|
15
|
+
|
|
15
16
|
"scripts": {
|
|
16
17
|
"start" : "node bin/index.js",
|
|
17
18
|
"qa" : "node bin/qa.js",
|
|
19
|
+
"qa:scan" : "node bin/qa.js scan --url=http://localhost:3000",
|
|
20
|
+
|
|
21
|
+
"qa:install" : "npx playwright install chromium",
|
|
18
22
|
"qa:auto" : "node bin/qa.js auto",
|
|
19
23
|
"qa:live" : "node bin/qa.js auto --continuous",
|
|
20
24
|
"qa:manual" : "node bin/qa.js manual",
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
"author": "W.A.H.ISHAN",
|
|
27
31
|
"license": "MIT",
|
|
28
32
|
"dependencies": {
|
|
33
|
+
"playwright": "^1.45.0",
|
|
29
34
|
"@babel/parser" : "^7.22.7",
|
|
30
35
|
"@babel/traverse" : "^7.22.8",
|
|
31
36
|
"@clack/prompts" : "^0.7.0",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Real accessibility checker using axe-core via Playwright
|
|
2
|
+
export class AccessibilityChecker {
|
|
3
|
+
#playwright;
|
|
4
|
+
#session;
|
|
5
|
+
|
|
6
|
+
constructor(playwright, session) {
|
|
7
|
+
this.#playwright = playwright;
|
|
8
|
+
this.#session = session;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async check(url) {
|
|
12
|
+
const browser = await this.#playwright.chromium.launch({ headless: true });
|
|
13
|
+
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|
14
|
+
const page = await context.newPage();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 });
|
|
18
|
+
|
|
19
|
+
// Inject axe-core from CDN for real WCAG testing
|
|
20
|
+
await page.addScriptTag({
|
|
21
|
+
url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Run real axe analysis
|
|
25
|
+
const axeResults = await page.evaluate(async () => {
|
|
26
|
+
return await window.axe.run(document, {
|
|
27
|
+
runOnly: {
|
|
28
|
+
type: 'tag',
|
|
29
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'best-practice'],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const violations = axeResults.violations.map(v => ({
|
|
35
|
+
id : v.id,
|
|
36
|
+
description : v.description,
|
|
37
|
+
help : v.help,
|
|
38
|
+
helpUrl : v.helpUrl,
|
|
39
|
+
impact : v.impact,
|
|
40
|
+
tags : v.tags,
|
|
41
|
+
category : v.tags.find(t => t.startsWith('wcag')) || 'best-practice',
|
|
42
|
+
nodes : v.nodes.length,
|
|
43
|
+
affectedNodes: v.nodes.slice(0, 3).map(n => ({
|
|
44
|
+
html : n.html,
|
|
45
|
+
target : n.target,
|
|
46
|
+
message: n.failureSummary,
|
|
47
|
+
})),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const passes = axeResults.passes.map(p => ({
|
|
51
|
+
id : p.id,
|
|
52
|
+
description: p.description,
|
|
53
|
+
nodes : p.nodes.length,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const incomplete = axeResults.incomplete.map(i => ({
|
|
57
|
+
id : i.id,
|
|
58
|
+
description: i.description,
|
|
59
|
+
nodes : i.nodes.length,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
pass : violations.length === 0,
|
|
64
|
+
violations,
|
|
65
|
+
passes,
|
|
66
|
+
incomplete,
|
|
67
|
+
score : passes.length > 0
|
|
68
|
+
? Math.round((passes.length / (passes.length + violations.length)) * 100)
|
|
69
|
+
: 0,
|
|
70
|
+
url,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return { pass: false, violations: [], passes: [], incomplete: [], error: err.message, url };
|
|
75
|
+
} finally {
|
|
76
|
+
await page.close().catch(() => {});
|
|
77
|
+
await context.close().catch(() => {});
|
|
78
|
+
await browser.close().catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Real API validator — every result from actual HTTP calls
|
|
2
|
+
import { shortId, formatBytes } from '../qa-engine.js';
|
|
3
|
+
|
|
4
|
+
const HTTP_TIMEOUT = 12_000;
|
|
5
|
+
|
|
6
|
+
export class RealAPIValidator {
|
|
7
|
+
#session;
|
|
8
|
+
|
|
9
|
+
constructor(session) { this.#session = session; }
|
|
10
|
+
|
|
11
|
+
async probe(url, method = 'GET', options = {}) {
|
|
12
|
+
const t0 = Date.now();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
|
|
17
|
+
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method,
|
|
20
|
+
signal : controller.signal,
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': 'Backlist-QA/12.0',
|
|
23
|
+
'Accept' : 'application/json, text/html, */*',
|
|
24
|
+
...options.headers,
|
|
25
|
+
},
|
|
26
|
+
redirect: 'follow',
|
|
27
|
+
});
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
|
|
30
|
+
const responseTime = Date.now() - t0;
|
|
31
|
+
const contentType = response.headers.get('content-type') || '';
|
|
32
|
+
const headers = {};
|
|
33
|
+
response.headers.forEach((v, k) => { headers[k] = v; });
|
|
34
|
+
|
|
35
|
+
let body = '';
|
|
36
|
+
let bodySize = 0;
|
|
37
|
+
try {
|
|
38
|
+
body = await response.text();
|
|
39
|
+
bodySize = new TextEncoder().encode(body).length;
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
let parsedBody = null;
|
|
43
|
+
if (contentType.includes('json')) {
|
|
44
|
+
try { parsedBody = JSON.parse(body); } catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const statusCode = response.status;
|
|
48
|
+
const pass = statusCode >= 200 && statusCode < 400;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
url,
|
|
52
|
+
method,
|
|
53
|
+
statusCode,
|
|
54
|
+
pass,
|
|
55
|
+
message : pass
|
|
56
|
+
? `${method} ${url} → ${statusCode} (${responseTime}ms)`
|
|
57
|
+
: `${method} ${url} → ${statusCode} FAIL`,
|
|
58
|
+
responseTime,
|
|
59
|
+
contentType,
|
|
60
|
+
headers,
|
|
61
|
+
body : body.slice(0, 2000),
|
|
62
|
+
parsedBody,
|
|
63
|
+
bodySize,
|
|
64
|
+
bodySizeStr: formatBytes(bodySize),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const duration = Date.now() - t0;
|
|
69
|
+
return {
|
|
70
|
+
url,
|
|
71
|
+
method,
|
|
72
|
+
statusCode : 0,
|
|
73
|
+
pass : false,
|
|
74
|
+
message : `Connection failed: ${err.message}`,
|
|
75
|
+
responseTime: duration,
|
|
76
|
+
contentType: '',
|
|
77
|
+
headers : {},
|
|
78
|
+
body : '',
|
|
79
|
+
error : err.message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async discoverFromNetworkLog(networkLog) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const apis = [];
|
|
87
|
+
|
|
88
|
+
for (const entry of networkLog) {
|
|
89
|
+
if (!entry.url || seen.has(entry.url)) continue;
|
|
90
|
+
if (!entry.url.includes('/api/') && !entry.url.includes('/graphql')) continue;
|
|
91
|
+
seen.add(entry.url);
|
|
92
|
+
apis.push({
|
|
93
|
+
id : shortId(),
|
|
94
|
+
url : entry.url,
|
|
95
|
+
method: entry.method || 'GET',
|
|
96
|
+
type : 'api',
|
|
97
|
+
source: 'network-log',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return apis;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate API contract: response structure, required fields
|
|
105
|
+
async validateContract(url, expectedSchema = {}) {
|
|
106
|
+
const result = await this.probe(url);
|
|
107
|
+
if (!result.pass || !result.parsedBody) return result;
|
|
108
|
+
|
|
109
|
+
const violations = [];
|
|
110
|
+
for (const [key, type] of Object.entries(expectedSchema)) {
|
|
111
|
+
const val = result.parsedBody[key];
|
|
112
|
+
if (val === undefined) {
|
|
113
|
+
violations.push(`Missing field: ${key}`);
|
|
114
|
+
} else if (type && typeof val !== type) {
|
|
115
|
+
violations.push(`Field ${key}: expected ${type}, got ${typeof val}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...result,
|
|
121
|
+
contractPass : violations.length === 0,
|
|
122
|
+
contractViolations: violations,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|