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 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
- // ── Version ───────────────────────────────────────────────────────────────
31
- const VERSION = '10.0.0';
32
-
33
- // ── CLI flags ─────────────────────────────────────────────────────────────
34
- const argv = process.argv.slice(2);
35
- const flags = new Set(argv.filter(a => a.startsWith('--')));
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
- // v10.0: URL flags
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 registerShutdownHandlers() {
54
- const shutdown = (signal) => {
55
- if (shuttingDown) return;
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(' ____ ___ ________ __ ____ ___________ ') + c1('║'));
79
- console.log(c1(' ║') + c2.bold(' / __ ) / | / ____/ //_/ / / / _/ ___/_ ') + c1('║'));
80
- console.log(c1(' ') + c2.bold(' / __ | / /| | / / / ,< / / / / \\__ \\ ') + 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
- const mem = process.memoryUsage();
90
- const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
91
-
92
- console.log(
93
- dim(' ') +
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
- // ── System health check ───────────────────────────────────────────────────
113
- async function systemHealthCheck() {
114
- const checks = [
115
- { name: 'Node.js ≥ 18', fn: () => {
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
- // ── Main ──────────────────────────────────────────────────────────────────
134
- async function main() {
135
- printQABanner();
136
- registerShutdownHandlers();
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
- await initQASystem();
139
- await systemHealthCheck();
140
-
141
- // ── Post-generation auto-run ─────────────────────────────────────────
142
- if (isPostGen) {
143
- const projectDir = posArgs[0] ? path.resolve(posArgs[0]) : process.cwd();
144
- const projectName = path.basename(projectDir);
145
- await autoRunPostGeneration({ projectDir, projectName });
146
- process.exit(0);
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
- // ── CI / positional arg mode ─────────────────────────────────────────
150
- const modeArg = posArgs[0]?.toLowerCase();
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 mode = modeArg ?? 'auto';
154
- if (!isCI) console.log(chalk.gray(` Mode: ${mode}${isContinuous ? ' (continuous)' : ''}${localUrlFlag ? ` → ${localUrlFlag}` : ''}\n`));
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
- if (!localUrlFlag && !prodUrlFlag) {
169
- console.error(chalk.red(' --url or --prod flag required for url mode.'));
170
- console.error(chalk.gray(' Example: node bin/qa.js url --url=http://localhost:3000 --prod=https://yoursite.com'));
171
- process.exit(1);
172
- }
173
- await runUrlQA({
174
- localUrl: localUrlFlag || undefined,
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: manual | auto | url | history | post-gen`));
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(` Backlist QA Platform v${VERSION} — HTTP + File + E2E Testing `));
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: 'url', label: '🌐 URL-Based QA Scan', hint: 'Probe localhost + production HTTP, security, SEO, a11y' },
201
- { value: 'manual', label: '🧪 Manual QA Testing', hint: 'Interactive test creation, custom cases, bug logging' },
202
- { value: 'auto', label: '🤖 Full Automated Scan', hint: 'E2E, security, auth, DB, performance — all modules' },
203
- { value: 'live', label: ' Live Continuous Monitoring', hint: 'Reruns every 30s — Ctrl+C to stop' },
204
- { value: 'post-gen', label: '🚀 Post-Generation Validation', hint: 'Validate a freshly generated backend project' },
205
- { value: 'history', label: '📜 View QA History', hint: 'Browse past runs, compare pass rates' },
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('QA session cancelled.'); process.exit(0); }
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 'url': {
212
- const localUrl = localUrlFlag || String(
213
- await p.text({ message: 'Localhost URL:', placeholder: 'http://localhost:3000', defaultValue: 'http://localhost:3000' })
214
- );
215
- const prodUrl = prodUrlFlag || String(
216
- await p.text({ message: 'Production URL (leave blank to skip):', placeholder: 'https://yoursite.com' }) || ''
217
- );
218
- if (p.isCancel(localUrl)) { p.cancel('Cancelled.'); process.exit(0); }
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 'auto':
226
- await runAutomatedQA({ continuous: false, localUrl: localUrlFlag || undefined, prodUrl: prodUrlFlag || undefined });
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 QA Fatal: ${err.message || err}`));
248
- if (err.stack && !isCI) console.error(chalk.gray(err.stack.split('\n').slice(1, 5).join('\n')));
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.2",
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
+ }