create-backlist 10.1.2 → 10.1.5

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.
Files changed (3) hide show
  1. package/bin/index.js +1759 -315
  2. package/package.json +1 -1
  3. package/src/qa/qa-engine.js +84 -180
@@ -76,7 +76,7 @@ async function getPlaywright() {
76
76
  }
77
77
 
78
78
  // ─────────────────────────────────────────────────────────────────────────
79
- // NEW v15: Image hash for visual regression
79
+ // Image hash for visual regression
80
80
  // ─────────────────────────────────────────────────────────────────────────
81
81
  function hashBuffer(buf) {
82
82
  return crypto.createHash('md5').update(buf).digest('hex');
@@ -101,30 +101,30 @@ export class QASession {
101
101
  a11yResults = [];
102
102
  seoResults = [];
103
103
  playwrightMode = false;
104
- // NEW v15 fields
105
- visualRegressions = [];
106
- cookieAudit = [];
107
- loadTestResults = [];
108
- brokenLinks = [];
109
- fontAudit = [];
110
- assetAudit = [];
111
- memorySnapshots = [];
112
- wsTests = [];
113
- darkModeResults = [];
114
- viewportResults = {};
115
- apiContracts = [];
116
- userFlowResults = [];
117
- redirectChains = [];
104
+ // v15 fields
105
+ visualRegressions = [];
106
+ cookieAudit = [];
107
+ loadTestResults = [];
108
+ brokenLinks = [];
109
+ fontAudit = [];
110
+ assetAudit = [];
111
+ memorySnapshots = [];
112
+ wsTests = [];
113
+ darkModeResults = [];
114
+ viewportResults = {};
115
+ apiContracts = [];
116
+ userFlowResults = [];
117
+ redirectChains = [];
118
118
  mixedContentIssues = [];
119
- cspViolations = [];
120
- thirdPartyScripts = [];
121
- errorPageTests = [];
122
- formTests = [];
123
- authTests = [];
124
- cacheHeaders = [];
125
- httpVersions = {};
126
- tlsInfo = {};
127
- dnsInfo = {};
119
+ cspViolations = [];
120
+ thirdPartyScripts = [];
121
+ errorPageTests = [];
122
+ formTests = [];
123
+ authTests = [];
124
+ cacheHeaders = [];
125
+ httpVersions = {};
126
+ tlsInfo = {};
127
+ dnsInfo = {};
128
128
 
129
129
  constructor(urls = {}) {
130
130
  this.id = `QA-${shortId().toUpperCase()}`;
@@ -156,7 +156,7 @@ export class QASession {
156
156
  }
157
157
 
158
158
  // ═══════════════════════════════════════════════════════════════════════════
159
- // HTTP Probe — real HTTP requests with v15 extras
159
+ // HTTP Probe — real HTTP requests
160
160
  // ═══════════════════════════════════════════════════════════════════════════
161
161
  async function httpProbe(url, { method = 'GET', timeout = 12000, headers = {}, body: reqBody = null, followRedirects = true } = {}) {
162
162
  const t0 = Date.now();
@@ -201,7 +201,7 @@ async function httpProbe(url, { method = 'GET', timeout = 12000, headers = {}, b
201
201
  }
202
202
 
203
203
  // ═══════════════════════════════════════════════════════════════════════════
204
- // NEW v15: Redirect Chain Analyzer
204
+ // Redirect Chain Analyzer
205
205
  // ═══════════════════════════════════════════════════════════════════════════
206
206
  async function analyzeRedirectChain(url) {
207
207
  const chain = [];
@@ -243,7 +243,7 @@ async function analyzeRedirectChain(url) {
243
243
  }
244
244
 
245
245
  // ═══════════════════════════════════════════════════════════════════════════
246
- // NEW v15: Load Test — concurrent requests
246
+ // Load Test — concurrent requests
247
247
  // ═══════════════════════════════════════════════════════════════════════════
248
248
  async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2000 } = {}) {
249
249
  const results = { requests: 0, errors: 0, timeouts: 0, responses: {} };
@@ -272,7 +272,7 @@ async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2
272
272
  } catch {
273
273
  results.errors++;
274
274
  }
275
- await sleep(50); // small breathing room
275
+ await sleep(50);
276
276
  }
277
277
  };
278
278
 
@@ -304,7 +304,7 @@ async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2
304
304
  }
305
305
 
306
306
  // ═══════════════════════════════════════════════════════════════════════════
307
- // NEW v15: Cookie Audit
307
+ // Cookie Audit
308
308
  // ═══════════════════════════════════════════════════════════════════════════
309
309
  async function runCookieAudit(url) {
310
310
  const r = await httpProbe(url);
@@ -343,7 +343,7 @@ async function runCookieAudit(url) {
343
343
  }
344
344
 
345
345
  // ═══════════════════════════════════════════════════════════════════════════
346
- // NEW v15: Broken Link Scanner (deep)
346
+ // Broken Link Scanner (deep)
347
347
  // ═══════════════════════════════════════════════════════════════════════════
348
348
  async function scanBrokenLinks(url, { maxLinks = 100 } = {}) {
349
349
  const r = await httpProbe(url);
@@ -389,7 +389,7 @@ async function scanBrokenLinks(url, { maxLinks = 100 } = {}) {
389
389
  }
390
390
 
391
391
  // ═══════════════════════════════════════════════════════════════════════════
392
- // NEW v15: API Contract Tester
392
+ // API Contract Tester
393
393
  // ═══════════════════════════════════════════════════════════════════════════
394
394
  async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields = [], method = 'GET', body = null, headers = {} } = {}) {
395
395
  const r = await httpProbe(endpoint, { method, body, headers, timeout: 10000 });
@@ -400,7 +400,7 @@ async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields
400
400
  }
401
401
  if (r.parsed && expectedFields.length > 0) {
402
402
  for (const field of expectedFields) {
403
- const hasField = field.includes('.')
403
+ const hasField = field.includes('.')
404
404
  ? field.split('.').reduce((obj, k) => obj?.[k], r.parsed) !== undefined
405
405
  : r.parsed[field] !== undefined || (Array.isArray(r.parsed) && r.parsed[0]?.[field] !== undefined);
406
406
  if (!hasField) issues.push(`Missing field: ${field}`);
@@ -422,7 +422,7 @@ async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields
422
422
  }
423
423
 
424
424
  // ═══════════════════════════════════════════════════════════════════════════
425
- // NEW v15: Form Interaction Tester (Playwright)
425
+ // Form Interaction Tester (Playwright)
426
426
  // ═══════════════════════════════════════════════════════════════════════════
427
427
  async function testForms(page, url) {
428
428
  const results = [];
@@ -436,19 +436,16 @@ async function testForms(page, url) {
436
436
  const submits = await form.$$('[type="submit"], button[type="submit"]');
437
437
  const hasSubmit = submits.length > 0;
438
438
 
439
- // Test required field validation
440
439
  let validationWorks = false;
441
440
  if (hasSubmit) {
442
441
  try {
443
442
  await submits[0].click({ timeout: 2000 });
444
443
  await page.waitForTimeout(300);
445
- // Check for validation messages
446
444
  const invalidFields = await page.$$(':invalid');
447
445
  validationWorks = invalidFields.length > 0 || (await page.evaluate(() => document.querySelector('.error, .invalid, [aria-invalid="true"]') !== null));
448
446
  } catch {}
449
447
  }
450
448
 
451
- // Test placeholder/label
452
449
  let labelCount = 0;
453
450
  for (const inp of inputs) {
454
451
  const id = await inp.getAttribute('id');
@@ -480,12 +477,11 @@ async function testForms(page, url) {
480
477
  }
481
478
 
482
479
  // ═══════════════════════════════════════════════════════════════════════════
483
- // NEW v15: Memory Leak Detector (Playwright)
480
+ // Memory Leak Detector (Playwright)
484
481
  // ═══════════════════════════════════════════════════════════════════════════
485
482
  async function detectMemoryLeaks(page, url) {
486
483
  const snapshots = [];
487
484
  try {
488
- // Snapshot 1: initial load
489
485
  const heap1 = await page.evaluate(() => {
490
486
  if (window.performance?.memory) {
491
487
  return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize };
@@ -494,7 +490,6 @@ async function detectMemoryLeaks(page, url) {
494
490
  });
495
491
  if (heap1) snapshots.push({ label: 'initial', ...heap1, time: 0 });
496
492
 
497
- // Simulate user interactions to trigger potential leaks
498
493
  await page.evaluate(() => {
499
494
  for (let i = 0; i < 5; i++) {
500
495
  window.dispatchEvent(new Event('scroll'));
@@ -503,7 +498,6 @@ async function detectMemoryLeaks(page, url) {
503
498
  });
504
499
  await page.waitForTimeout(1000);
505
500
 
506
- // Navigate away and back
507
501
  const currentUrl = page.url();
508
502
  await page.goto('about:blank', { waitUntil: 'load' }).catch(() => {});
509
503
  await page.goto(currentUrl, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
@@ -532,19 +526,17 @@ async function detectMemoryLeaks(page, url) {
532
526
  }
533
527
 
534
528
  // ═══════════════════════════════════════════════════════════════════════════
535
- // NEW v15: Dark Mode Tester (Playwright)
529
+ // Dark Mode Tester (Playwright)
536
530
  // ═══════════════════════════════════════════════════════════════════════════
537
531
  async function testDarkMode(page, url, screenshotDir, sessionId) {
538
532
  const results = {};
539
533
  try {
540
- // Light mode screenshot already taken — test dark mode
541
534
  await page.emulateMedia({ colorScheme: 'dark' });
542
535
  await page.waitForTimeout(800);
543
536
  const darkName = `${sessionId}-dark-${shortId()}.png`;
544
537
  const darkPath = path.join(screenshotDir, darkName);
545
538
  await page.screenshot({ path: darkPath, fullPage: false });
546
539
 
547
- // Check if dark mode actually changes anything
548
540
  const hasMediaQuery = await page.evaluate(() => {
549
541
  const sheets = [...document.styleSheets];
550
542
  for (const sheet of sheets) {
@@ -558,7 +550,6 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
558
550
  return false;
559
551
  });
560
552
 
561
- // Check body background color changes
562
553
  const darkBg = await page.evaluate(() => {
563
554
  return window.getComputedStyle(document.body).backgroundColor;
564
555
  });
@@ -567,7 +558,6 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
567
558
  results.hasMediaQuery = hasMediaQuery;
568
559
  results.supportsDark = hasMediaQuery;
569
560
 
570
- // Reset to light
571
561
  await page.emulateMedia({ colorScheme: 'light' });
572
562
  await page.waitForTimeout(300);
573
563
 
@@ -584,11 +574,10 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
584
574
  }
585
575
 
586
576
  // ═══════════════════════════════════════════════════════════════════════════
587
- // NEW v15: Third-Party Script Auditor (Playwright)
577
+ // Third-Party Script Auditor (Playwright)
588
578
  // ═══════════════════════════════════════════════════════════════════════════
589
579
  async function auditThirdPartyScripts(page) {
590
580
  const origin = new URL(page.url()).origin;
591
- const scripts = [];
592
581
  const requests = [];
593
582
 
594
583
  const handler = (req) => {
@@ -609,7 +598,6 @@ async function auditThirdPartyScripts(page) {
609
598
  await page.waitForTimeout(2000);
610
599
  page.off('request', handler);
611
600
 
612
- // Deduplicate by domain
613
601
  const domainMap = {};
614
602
  for (const r of requests) {
615
603
  if (!domainMap[r.domain]) domainMap[r.domain] = { ...r, count: 0 };
@@ -642,7 +630,7 @@ function classifyThirdParty(hostname) {
642
630
  }
643
631
 
644
632
  // ═══════════════════════════════════════════════════════════════════════════
645
- // NEW v15: Font & Asset Auditor (Playwright)
633
+ // Font & Asset Auditor (Playwright)
646
634
  // ═══════════════════════════════════════════════════════════════════════════
647
635
  async function auditFontsAndAssets(page) {
648
636
  return await page.evaluate(() => {
@@ -673,12 +661,10 @@ async function auditFontsAndAssets(page) {
673
661
  }
674
662
  }
675
663
 
676
- // Font analysis
677
664
  const fontFaces = document.fonts ? [...document.fonts].map(f => ({
678
665
  family: f.family, style: f.style, weight: f.weight, status: f.status,
679
666
  })) : [];
680
667
 
681
- // Image format analysis
682
668
  const images = [...document.images].map(img => ({
683
669
  src: img.src?.split('/').pop().slice(0, 60),
684
670
  naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight,
@@ -700,7 +686,7 @@ async function auditFontsAndAssets(page) {
700
686
  }
701
687
 
702
688
  // ═══════════════════════════════════════════════════════════════════════════
703
- // NEW v15: User Flow Simulator (Playwright)
689
+ // User Flow Simulator (Playwright)
704
690
  // ═══════════════════════════════════════════════════════════════════════════
705
691
  async function simulateUserFlow(page, url) {
706
692
  const steps = [];
@@ -770,7 +756,7 @@ async function simulateUserFlow(page, url) {
770
756
  }
771
757
 
772
758
  // ═══════════════════════════════════════════════════════════════════════════
773
- // NEW v15: Multi-Viewport Screenshot + Layout Tester (Playwright)
759
+ // Multi-Viewport Screenshot + Layout Tester (Playwright)
774
760
  // ═══════════════════════════════════════════════════════════════════════════
775
761
  async function testAllViewports(page, url, screenshotDir, sessionId) {
776
762
  const results = {};
@@ -783,11 +769,9 @@ async function testAllViewports(page, url, screenshotDir, sessionId) {
783
769
  const fpath = path.join(screenshotDir, name);
784
770
  await page.screenshot({ path: fpath, fullPage: false });
785
771
 
786
- // Check for overflow/horizontal scroll
787
772
  const hasHorizontalScroll = await page.evaluate(() =>
788
773
  document.documentElement.scrollWidth > document.documentElement.clientWidth
789
774
  );
790
- // Check font size not too small
791
775
  const minFontSize = await page.evaluate(() => {
792
776
  const els = [...document.querySelectorAll('p, span, a, li, td')].slice(0, 20);
793
777
  return Math.min(...els.map(el => parseFloat(window.getComputedStyle(el).fontSize) || 16));
@@ -807,13 +791,12 @@ async function testAllViewports(page, url, screenshotDir, sessionId) {
807
791
  results[key] = { label: vp.label, width: vp.width, height: vp.height, error: err.message, passed: false };
808
792
  }
809
793
  }
810
- // Reset to desktop
811
794
  await page.setViewportSize({ width: 1280, height: 900 });
812
795
  return results;
813
796
  }
814
797
 
815
798
  // ═══════════════════════════════════════════════════════════════════════════
816
- // NEW v15: Cache Headers Auditor
799
+ // Cache Headers Auditor
817
800
  // ═══════════════════════════════════════════════════════════════════════════
818
801
  async function auditCacheHeaders(url) {
819
802
  const r = await httpProbe(url);
@@ -848,7 +831,7 @@ async function auditCacheHeaders(url) {
848
831
  }
849
832
 
850
833
  // ═══════════════════════════════════════════════════════════════════════════
851
- // NEW v15: Mixed Content & CSP Violation Checker (Playwright)
834
+ // Mixed Content & CSP Violation Checker (Playwright)
852
835
  // ═══════════════════════════════════════════════════════════════════════════
853
836
  async function checkMixedContent(page) {
854
837
  const mixed = [];
@@ -867,7 +850,7 @@ async function checkMixedContent(page) {
867
850
  }
868
851
 
869
852
  // ═══════════════════════════════════════════════════════════════════════════
870
- // NEW v15: Error Page Tester (404, 500)
853
+ // Error Page Tester (404, 500)
871
854
  // ═══════════════════════════════════════════════════════════════════════════
872
855
  async function testErrorPages(baseUrl) {
873
856
  const tests = [
@@ -898,7 +881,7 @@ async function testErrorPages(baseUrl) {
898
881
  }
899
882
 
900
883
  // ═══════════════════════════════════════════════════════════════════════════
901
- // NEW v15: HTTP Version & TLS Inspector
884
+ // HTTP Version & TLS Inspector
902
885
  // ═══════════════════════════════════════════════════════════════════════════
903
886
  async function inspectHTTPVersion(url) {
904
887
  const r = await httpProbe(url);
@@ -910,7 +893,7 @@ async function inspectHTTPVersion(url) {
910
893
  return {
911
894
  url, isHTTPS,
912
895
  altSvc: altSvc || null,
913
- likelyHTTP2: hasH2 || isHTTPS, // Most modern HTTPS servers use H2
896
+ likelyHTTP2: hasH2 || isHTTPS,
914
897
  likelyHTTP3: hasH3,
915
898
  hsts: r.headers['strict-transport-security'] || null,
916
899
  issues: [
@@ -920,7 +903,7 @@ async function inspectHTTPVersion(url) {
920
903
  }
921
904
 
922
905
  // ═══════════════════════════════════════════════════════════════════════════
923
- // PLAYWRIGHT REAL BROWSER ENGINE v15 — Enhanced
906
+ // PLAYWRIGHT REAL BROWSER ENGINE v15
924
907
  // ═══════════════════════════════════════════════════════════════════════════
925
908
  async function runPlaywrightScan(url, session, dash, options = {}) {
926
909
  const chromium = await getPlaywright();
@@ -966,7 +949,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
966
949
 
967
950
  page = await context.newPage();
968
951
 
969
- // ── Mixed Content & CSP violations ──────────────────────────────────
970
952
  const mixedContent = [];
971
953
  const cspViolations2 = [];
972
954
  page.on('console', (msg) => {
@@ -981,14 +963,12 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
981
963
  }
982
964
  });
983
965
 
984
- // ── Capture JS errors ────────────────────────────────────────────────
985
966
  page.on('pageerror', (err) => {
986
967
  const entry = { message: err.message, stack: err.stack, url: page.url(), timestamp: Date.now() };
987
968
  results.jsErrors.push(entry);
988
969
  session.consoleErrors.push({ type: 'pageerror', text: err.message, url: page.url() });
989
970
  });
990
971
 
991
- // ── Network monitoring ───────────────────────────────────────────────
992
972
  const requestTimings = new Map();
993
973
  page.on('request', (req) => {
994
974
  requestTimings.set(req.url(), Date.now());
@@ -1015,7 +995,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1015
995
  }
1016
996
  });
1017
997
 
1018
- // ── Navigate ─────────────────────────────────────────────────────────
1019
998
  const navStart = Date.now();
1020
999
  const response = await page.goto(url, {
1021
1000
  waitUntil: 'networkidle', timeout: 30000,
@@ -1029,7 +1008,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1029
1008
 
1030
1009
  await fs.ensureDir(SCREENSHOT_DIR);
1031
1010
 
1032
- // ── 1. Desktop Screenshot ────────────────────────────────────────────
1011
+ // 1. Desktop Screenshot
1033
1012
  const desktopName = `${session.id}-desktop-${shortId()}.png`;
1034
1013
  const desktopPath = path.join(SCREENSHOT_DIR, desktopName);
1035
1014
  await page.screenshot({ path: desktopPath, fullPage: true });
@@ -1037,7 +1016,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1037
1016
  session.screenshots.push({ path: desktopPath, name: desktopName, type: 'desktop', url });
1038
1017
  dash?.log(chalk.green(` 📸 Desktop screenshot: ${desktopName}`));
1039
1018
 
1040
- // ── 2. Multi-Viewport Testing (v15) ──────────────────────────────────
1019
+ // 2. Multi-Viewport Testing
1041
1020
  dash?.log(chalk.cyan(' 📱 Testing all viewports...'));
1042
1021
  const vpResults = await testAllViewports(page, url, SCREENSHOT_DIR, session.id);
1043
1022
  results.viewportResults = vpResults;
@@ -1050,7 +1029,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1050
1029
  const vpIssues = Object.values(vpResults).filter(v => !v.passed);
1051
1030
  dash?.log(chalk.green(` ✓ Viewports: ${Object.keys(vpResults).length - vpIssues.length}/${Object.keys(vpResults).length} passed`));
1052
1031
 
1053
- // ── 3. Dark Mode Test (v15) ───────────────────────────────────────────
1032
+ // 3. Dark Mode Test
1054
1033
  dash?.log(chalk.cyan(' 🌙 Testing dark mode...'));
1055
1034
  const darkResult = await testDarkMode(page, url, SCREENSHOT_DIR, session.id);
1056
1035
  results.darkMode = darkResult;
@@ -1059,9 +1038,8 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1059
1038
  session.screenshots.push({ path: darkResult.dark.screenshotPath, name: darkResult.dark.screenshotName, type: 'dark-mode', url });
1060
1039
  }
1061
1040
 
1062
- // ── 4. Real Web Vitals ────────────────────────────────────────────────
1041
+ // 4. Real Web Vitals
1063
1042
  dash?.log(chalk.cyan(' ⚡ Measuring real Web Vitals...'));
1064
- // Navigate fresh for clean vitals
1065
1043
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
1066
1044
  const vitals = await page.evaluate(() => {
1067
1045
  return new Promise((resolve) => {
@@ -1102,7 +1080,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1102
1080
  results.vitals = { ...vitals, ...navTiming, navDuration };
1103
1081
  dash?.log(chalk.green(` ✓ Vitals: TTFB=${navTiming.ttfb||'?'}ms LCP=${vitals.lcp||'?'}ms FCP=${vitals.fcp||'?'}ms CLS=${vitals.cls??'?'}`));
1104
1082
 
1105
- // ── 5. Memory Leak Detection (v15) ────────────────────────────────────
1083
+ // 5. Memory Leak Detection
1106
1084
  dash?.log(chalk.cyan(' 🧠 Detecting memory leaks...'));
1107
1085
  const memResult = await detectMemoryLeaks(page, url);
1108
1086
  results.memoryLeak = memResult;
@@ -1111,7 +1089,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1111
1089
  dash?.log(chalk.yellow(` ⚠ Memory leak detected: +${memResult.growthMB}MB`));
1112
1090
  }
1113
1091
 
1114
- // ── 6. DOM Checks ────────────────────────────────────────────────────
1092
+ // 6. DOM Checks
1115
1093
  dash?.log(chalk.cyan(' 🔍 Running DOM checks...'));
1116
1094
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
1117
1095
  const domChecks = await page.evaluate(() => {
@@ -1136,7 +1114,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1136
1114
  checks.push({ name: 'Viewport meta', pass: !!vp, value: vp?.content || 'missing' });
1137
1115
  const bodyStyle = window.getComputedStyle(document.body);
1138
1116
  checks.push({ name: 'Body has styles', pass: !!bodyStyle.backgroundColor || !!bodyStyle.color, value: 'CSS applied' });
1139
- // NEW v15 DOM checks
1140
1117
  const skipLink = document.querySelector('a[href="#main"], a[href="#content"], .skip-link');
1141
1118
  checks.push({ name: 'Skip navigation link', pass: !!skipLink, value: skipLink ? 'Present' : 'Missing (accessibility)' });
1142
1119
  const mainEl = document.querySelector('main, [role="main"]');
@@ -1161,25 +1138,25 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1161
1138
  results.domChecks = domChecks;
1162
1139
  dash?.log(chalk.green(` ✓ DOM: ${domChecks.filter(c => c.pass).length}/${domChecks.length} checks passed`));
1163
1140
 
1164
- // ── 7. Form Tests (v15) ───────────────────────────────────────────────
1141
+ // 7. Form Tests
1165
1142
  dash?.log(chalk.cyan(' 📝 Testing forms...'));
1166
1143
  const formResults = await testForms(page, url);
1167
1144
  results.forms = formResults;
1168
1145
  session.formTests.push(...formResults.map(f => ({ url, ...f })));
1169
1146
 
1170
- // ── 8. Third-Party Script Audit (v15) ─────────────────────────────────
1147
+ // 8. Third-Party Script Audit
1171
1148
  dash?.log(chalk.cyan(' 📦 Auditing third-party scripts...'));
1172
1149
  const thirdPartyScripts = await auditThirdPartyScripts(page);
1173
1150
  results.thirdParty = thirdPartyScripts;
1174
1151
  session.thirdPartyScripts.push(...thirdPartyScripts.map(s => ({ url, ...s })));
1175
1152
 
1176
- // ── 9. Font & Asset Audit (v15) ───────────────────────────────────────
1153
+ // 9. Font & Asset Audit
1177
1154
  dash?.log(chalk.cyan(' 🔤 Auditing fonts and assets...'));
1178
1155
  const assetData = await auditFontsAndAssets(page);
1179
1156
  results.fonts = assetData;
1180
1157
  session.assetAudit.push({ url, ...assetData });
1181
1158
 
1182
- // ── 10. Interaction Tests ─────────────────────────────────────────────
1159
+ // 10. Interaction Tests
1183
1160
  dash?.log(chalk.cyan(' 🖱️ Testing interactions...'));
1184
1161
  const interactions = [];
1185
1162
  const buttonCount = await page.locator('button:visible').count().catch(() => 0);
@@ -1206,14 +1183,12 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1206
1183
  const firstLink = page.locator('a:visible').first();
1207
1184
  if (await firstLink.count() > 0) { await firstLink.hover(); interactions.push({ name: 'Link hover', pass: true, value: 'Hover works' }); }
1208
1185
  } catch { interactions.push({ name: 'Link hover', pass: false, value: 'Hover failed' }); }
1209
- // NEW v15: right-click test
1210
1186
  try {
1211
1187
  await page.mouse.click(640, 400, { button: 'right' });
1212
1188
  await page.waitForTimeout(200);
1213
1189
  await page.keyboard.press('Escape');
1214
1190
  interactions.push({ name: 'Right-click (context menu)', pass: true, value: 'Works' });
1215
1191
  } catch { interactions.push({ name: 'Right-click', pass: false, value: 'Failed' }); }
1216
- // NEW v15: copy text test
1217
1192
  try {
1218
1193
  await page.keyboard.press('Control+a');
1219
1194
  await page.waitForTimeout(100);
@@ -1223,14 +1198,14 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1223
1198
  results.interactions = interactions;
1224
1199
  dash?.log(chalk.green(` ✓ Interactions: ${interactions.filter(i => i.pass).length}/${interactions.length} passed`));
1225
1200
 
1226
- // ── 11. User Flow Simulation (v15) ────────────────────────────────────
1201
+ // 11. User Flow Simulation
1227
1202
  dash?.log(chalk.cyan(' 🧑‍💻 Simulating user flow...'));
1228
1203
  const flowResult = await simulateUserFlow(page, url);
1229
1204
  results.userFlow = flowResult;
1230
1205
  session.userFlowResults.push(flowResult);
1231
1206
  dash?.log(chalk.green(` ✓ User flow: ${flowResult.passed}/${flowResult.steps.length} steps passed`));
1232
1207
 
1233
- // ── 12. Resource Analysis ─────────────────────────────────────────────
1208
+ // 12. Resource Analysis
1234
1209
  const resourceStats = await page.evaluate(() => {
1235
1210
  const entries = performance.getEntriesByType('resource');
1236
1211
  const byType = {};
@@ -1251,7 +1226,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1251
1226
  results.mixedContent = mixedContent;
1252
1227
  results.cspViolations = cspViolations2;
1253
1228
 
1254
- // Store mixed content/CSP in session
1255
1229
  session.mixedContentIssues.push(...mixedContent.map(m => ({ url, text: m })));
1256
1230
  session.cspViolations.push(...cspViolations2.map(c => ({ url, text: c })));
1257
1231
 
@@ -1330,7 +1304,7 @@ async function crawlSite(baseUrl, { maxPages = 60, onRoute } = {}) {
1330
1304
  }
1331
1305
  }
1332
1306
 
1333
- // Common paths probe (v15 extended)
1307
+ // Common paths probe
1334
1308
  const commonPaths = [
1335
1309
  '/api/health', '/health', '/api/status', '/api/v1/health',
1336
1310
  '/api/docs', '/robots.txt', '/sitemap.xml', '/manifest.json',
@@ -1358,7 +1332,7 @@ async function crawlSite(baseUrl, { maxPages = 60, onRoute } = {}) {
1358
1332
  }
1359
1333
 
1360
1334
  // ═══════════════════════════════════════════════════════════════════════════
1361
- // Security Scanner v15 — Extended
1335
+ // Security Scanner v15
1362
1336
  // ═══════════════════════════════════════════════════════════════════════════
1363
1337
  async function runSecurityScan(url) {
1364
1338
  const findings = [];
@@ -1388,7 +1362,6 @@ async function runSecurityScan(url) {
1388
1362
  validate: v => !v || (!v.includes('/') && !/\d+\.\d+/.test(v)), rec: 'Genericize Server header' },
1389
1363
  { id: 'xpb', name: 'X-Powered-By hidden', header: 'x-powered-by', sev: 'P2',
1390
1364
  validate: v => !v, rec: 'Remove X-Powered-By header' },
1391
- // NEW v15
1392
1365
  { id: 'coep', name: 'Cross-Origin-Embedder-Policy', header: 'cross-origin-embedder-policy', sev: 'P3',
1393
1366
  validate: v => !!v, rec: 'Add COEP for isolation' },
1394
1367
  { id: 'coop', name: 'Cross-Origin-Opener-Policy', header: 'cross-origin-opener-policy', sev: 'P3',
@@ -1421,7 +1394,6 @@ async function runSecurityScan(url) {
1421
1394
  recommendation: 'Never combine CORS * with allow-credentials',
1422
1395
  });
1423
1396
 
1424
- // NEW v15: Check for version disclosure in other headers
1425
1397
  const versionHeaders = ['x-aspnet-version', 'x-aspnetmvc-version', 'x-drupal-cache', 'x-generator'];
1426
1398
  for (const vh of versionHeaders) {
1427
1399
  if (h[vh]) findings.push({
@@ -1444,7 +1416,6 @@ async function runSecurityScan(url) {
1444
1416
  { path: '/api/openapi.json', name: 'OpenAPI docs exposed' },
1445
1417
  { path: '/config.json', name: 'config.json exposed' },
1446
1418
  { path: '/debug', name: 'Debug endpoint' },
1447
- // NEW v15
1448
1419
  { path: '/.DS_Store', name: '.DS_Store exposed' },
1449
1420
  { path: '/wp-config.php', name: 'WordPress config' },
1450
1421
  { path: '/package.json', name: 'package.json exposed' },
@@ -1474,7 +1445,7 @@ async function runSecurityScan(url) {
1474
1445
  }
1475
1446
 
1476
1447
  // ═══════════════════════════════════════════════════════════════════════════
1477
- // SEO Scanner v15 — Extended
1448
+ // SEO Scanner v15
1478
1449
  // ═══════════════════════════════════════════════════════════════════════════
1479
1450
  async function runSEOScan(url) {
1480
1451
  const t0 = Date.now();
@@ -1535,7 +1506,6 @@ async function runSEOScan(url) {
1535
1506
  checks.push({ name: 'Server response time', pass: rt < 800, severity: rt > 2000 ? 'P1' : 'P2',
1536
1507
  category: 'performance-seo', detail: `TTFB: ${rt}ms (Google: <800ms)` });
1537
1508
 
1538
- // NEW v15: Heading hierarchy
1539
1509
  const headings = (html.match(/<h[1-6][^>]*>/gi) || []).map(h => parseInt(h[2]));
1540
1510
  let hierOk = true;
1541
1511
  for (let i = 1; i < headings.length; i++) {
@@ -1543,7 +1513,6 @@ async function runSEOScan(url) {
1543
1513
  }
1544
1514
  checks.push({ name: 'Heading hierarchy', pass: hierOk, severity: 'P2', category: 'structure', detail: hierOk ? 'Headings in order' : 'Skipped heading levels' });
1545
1515
 
1546
- // NEW v15: noindex check
1547
1516
  const noindex = has(/<meta[^>]+name=["']robots["'][^>]+content=["'][^"']*noindex/i);
1548
1517
  checks.push({ name: 'Not noindexed', pass: !noindex, severity: noindex ? 'P1' : 'INFO', category: 'crawling', detail: noindex ? 'Page is noindexed!' : 'Indexable' });
1549
1518
 
@@ -1562,7 +1531,7 @@ async function runSEOScan(url) {
1562
1531
  }
1563
1532
 
1564
1533
  // ═══════════════════════════════════════════════════════════════════════════
1565
- // Accessibility Scanner v15 — Extended
1534
+ // Accessibility Scanner v15
1566
1535
  // ═══════════════════════════════════════════════════════════════════════════
1567
1536
  async function runA11yScan(url) {
1568
1537
  const r = await httpProbe(url, { timeout: 12000 });
@@ -1578,7 +1547,6 @@ async function runA11yScan(url) {
1578
1547
  { id: 'h1-present', impact: 'moderate', test: () => !/<h1[^>]*>/i.test(html), pass: 'H1 heading present', desc: 'Page should have H1' },
1579
1548
  { id: 'link-text', impact: 'serious', test: () => /<a[^>]*>\s*<\/a>/i.test(html), pass: 'Links have text', desc: 'Links must have discernible text' },
1580
1549
  { id: 'form-labels', impact: 'critical', test: () => /<input(?![^>]*(?:aria-label|aria-labelledby|id=))[^>]*type=(?!"hidden")[^>]*>/i.test(html), pass: 'Form inputs have labels', desc: 'Form elements must have labels' },
1581
- // NEW v15
1582
1550
  { id: 'skip-nav', impact: 'moderate', test: () => !/<a[^>]*href=["']#(?:main|content|skip)[^"']*["']/i.test(html), pass: 'Skip nav link', desc: 'Page should have skip navigation link' },
1583
1551
  { id: 'table-headers', impact: 'serious', test: () => /<table/i.test(html) && !/<th/i.test(html), pass: 'Tables have headers', desc: 'Tables must have header cells' },
1584
1552
  { id: 'input-purpose', impact: 'serious', test: () => /<input[^>]*type=["'](email|tel|name)[^"']*["']/i.test(html) && !/<input[^>]*autocomplete/i.test(html), pass: 'Inputs have autocomplete', desc: 'Contact inputs should have autocomplete' },
@@ -1603,7 +1571,7 @@ async function runA11yScan(url) {
1603
1571
  }
1604
1572
 
1605
1573
  // ═══════════════════════════════════════════════════════════════════════════
1606
- // AI Bug Classifier v15 — Enhanced patterns
1574
+ // AI Bug Classifier v15
1607
1575
  // ═══════════════════════════════════════════════════════════════════════════
1608
1576
  const SEV_PATTERNS = {
1609
1577
  P0: [/security|auth.*bypass|sql.inject|xss|rce|exposed.*secret|password.*leak|critical|\.env.*exposed|git.*config.*exposed|database.*dump/i, /crash|fatal|500|server.*down|data.*loss|memory.*leak.*critical/i],
@@ -1647,7 +1615,7 @@ function classifyBug(bug) {
1647
1615
  }
1648
1616
 
1649
1617
  // ═══════════════════════════════════════════════════════════════════════════
1650
- // Terminal Dashboard v15 — Enhanced live display
1618
+ // Terminal Dashboard v15
1651
1619
  // ═══════════════════════════════════════════════════════════════════════════
1652
1620
  class TerminalDashboard {
1653
1621
  #session; #lines = 0; #active = false; #timer = null;
@@ -1777,7 +1745,7 @@ class TerminalDashboard {
1777
1745
  }
1778
1746
 
1779
1747
  // ═══════════════════════════════════════════════════════════════════════════
1780
- // HTML Report Builder v15 — Ultra Rich
1748
+ // HTML Report Builder v15
1781
1749
  // ═══════════════════════════════════════════════════════════════════════════
1782
1750
  function buildHTMLReport(session) {
1783
1751
  const summary = session.getSummary();
@@ -1799,14 +1767,6 @@ function buildHTMLReport(session) {
1799
1767
 
1800
1768
  const esc = (s) => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1801
1769
 
1802
- // ── Screenshot gallery ───────────────────────────────────────────────────
1803
- const screenshotsByType = {};
1804
- for (const sc of session.screenshots) {
1805
- const t = sc.type || 'other';
1806
- if (!screenshotsByType[t]) screenshotsByType[t] = [];
1807
- screenshotsByType[t].push(sc);
1808
- }
1809
-
1810
1770
  const screenshotCards = session.screenshots.length
1811
1771
  ? session.screenshots.map(sc => {
1812
1772
  let imgTag = '';
@@ -1831,7 +1791,6 @@ function buildHTMLReport(session) {
1831
1791
  }).join('')
1832
1792
  : '<p class="no-data">No screenshots (Playwright not available)</p>';
1833
1793
 
1834
- // ── Test rows ─────────────────────────────────────────────────────────────
1835
1794
  const testRows = session.results.map(r => `
1836
1795
  <tr class="result-row" data-type="${r.type}" data-status="${r.status}">
1837
1796
  <td>${esc(r.name)}</td>
@@ -1842,7 +1801,6 @@ function buildHTMLReport(session) {
1842
1801
  <td class="err-cell">${r.message ? `<details><summary>Details</summary><pre>${esc(String(r.message).slice(0,500))}</pre></details>` : '✓'}</td>
1843
1802
  </tr>`).join('');
1844
1803
 
1845
- // ── Bug cards ─────────────────────────────────────────────────────────────
1846
1804
  const bugCards = session.bugs.length
1847
1805
  ? session.bugs.map(b => `
1848
1806
  <div class="bug-card sev-border-${(b.aiSeverity||b.severity||'p3').toLowerCase()}" data-severity="${b.aiSeverity||b.severity}">
@@ -1859,7 +1817,6 @@ function buildHTMLReport(session) {
1859
1817
  </div>`).join('')
1860
1818
  : '<p class="no-data">No bugs detected 🎉</p>';
1861
1819
 
1862
- // ── Route rows ────────────────────────────────────────────────────────────
1863
1820
  const routeRows = session.routeMap.map(r => `
1864
1821
  <tr>
1865
1822
  <td><code class="url">${esc(r.url)}</code></td>
@@ -1870,7 +1827,6 @@ function buildHTMLReport(session) {
1870
1827
  <td>${r.error ? `<span class="fail">${esc(r.error)}</span>` : '✓'}</td>
1871
1828
  </tr>`).join('');
1872
1829
 
1873
- // ── Security rows ─────────────────────────────────────────────────────────
1874
1830
  const secRows = session.secFindings.map(f => `
1875
1831
  <tr class="${f.pass ? '' : 'fail-row'}">
1876
1832
  <td>${esc(f.check)}</td>
@@ -1881,7 +1837,6 @@ function buildHTMLReport(session) {
1881
1837
  <td>${f.recommendation ? `<span class="rec">${esc(f.recommendation)}</span>` : '–'}</td>
1882
1838
  </tr>`).join('');
1883
1839
 
1884
- // ── SEO section ───────────────────────────────────────────────────────────
1885
1840
  const seoSection = session.seoResults.map(r => `
1886
1841
  <div class="seo-page">
1887
1842
  <div class="seo-header">
@@ -1898,7 +1853,6 @@ function buildHTMLReport(session) {
1898
1853
  </table>
1899
1854
  </div>`).join('') || '<p class="no-data">No SEO scans</p>';
1900
1855
 
1901
- // ── A11y section ──────────────────────────────────────────────────────────
1902
1856
  const a11ySection = session.a11yResults.map(r => `
1903
1857
  <div class="a11y-page">
1904
1858
  <div class="a11y-header">
@@ -1912,7 +1866,6 @@ function buildHTMLReport(session) {
1912
1866
  </div>`).join('') || '<p class="no-data">No violations ✓</p>'}
1913
1867
  </div>`).join('') || '<p class="no-data">No accessibility scans</p>';
1914
1868
 
1915
- // ── Performance section ───────────────────────────────────────────────────
1916
1869
  const vitalCard = (name, value, threshold, unit) => {
1917
1870
  const na = value === null || value === undefined;
1918
1871
  const pass2 = !na && value <= threshold;
@@ -1965,7 +1918,6 @@ function buildHTMLReport(session) {
1965
1918
  </div>`;
1966
1919
  }).join('') || '<p class="no-data">No performance data</p>';
1967
1920
 
1968
- // ── NEW v15: Load Test section ────────────────────────────────────────────
1969
1921
  const loadTestSection = session.loadTestResults.length
1970
1922
  ? session.loadTestResults.map(lt => `
1971
1923
  <div class="load-test-card ${lt.passed ? 'lt-pass' : 'lt-fail'}">
@@ -1980,9 +1932,8 @@ function buildHTMLReport(session) {
1980
1932
  <p style="color:#94a3b8;font-size:.8rem;margin-top:.75rem">${lt.requests} requests · ${lt.errors} errors · ${lt.timeouts} timeouts · ${formatDuration(lt.duration)}</p>
1981
1933
  <p style="color:#94a3b8;font-size:.78rem">Status codes: ${Object.entries(lt.responses||{}).map(([k,v]) => `${k}: ${v}`).join(', ')}</p>
1982
1934
  </div>`).join('')
1983
- : '<p class="no-data">Load test not run (use runUrlQA with loadTest:true)</p>';
1935
+ : '<p class="no-data">Load test not run</p>';
1984
1936
 
1985
- // ── NEW v15: Viewport section ─────────────────────────────────────────────
1986
1937
  const vpSection = Object.keys(session.viewportResults || {}).length
1987
1938
  ? `<div class="viewport-grid">${Object.entries(session.viewportResults).map(([key, vp]) => `
1988
1939
  <div class="vp-card ${vp.passed ? '' : 'vp-fail'}">
@@ -1993,7 +1944,6 @@ function buildHTMLReport(session) {
1993
1944
  </div>`).join('')}</div>`
1994
1945
  : '<p class="no-data">No viewport tests (Playwright required)</p>';
1995
1946
 
1996
- // ── NEW v15: Broken links section ─────────────────────────────────────────
1997
1947
  const brokenLinksSection = session.brokenLinks.length
1998
1948
  ? session.brokenLinks.map(bl => `
1999
1949
  <div class="card" style="margin-bottom:1rem">
@@ -2009,7 +1959,6 @@ function buildHTMLReport(session) {
2009
1959
  </div>`).join('')
2010
1960
  : '<p class="no-data">No broken link scans run</p>';
2011
1961
 
2012
- // ── NEW v15: Cookie audit ─────────────────────────────────────────────────
2013
1962
  const cookieSection = session.cookieAudit.length
2014
1963
  ? session.cookieAudit.map(ca => `
2015
1964
  <div class="card" style="margin-bottom:1rem">
@@ -2027,7 +1976,6 @@ function buildHTMLReport(session) {
2027
1976
  </div>`).join('')
2028
1977
  : '<p class="no-data">No cookie audits</p>';
2029
1978
 
2030
- // ── NEW v15: Third-party scripts ──────────────────────────────────────────
2031
1979
  const thirdPartySection = session.thirdPartyScripts.length
2032
1980
  ? `<table>
2033
1981
  <thead><tr><th>Vendor</th><th>Domain</th><th>Count</th><th>URL</th></tr></thead>
@@ -2040,7 +1988,6 @@ function buildHTMLReport(session) {
2040
1988
  </table>`
2041
1989
  : '<p class="no-data">No third-party scripts detected</p>';
2042
1990
 
2043
- // ── NEW v15: User Flow section ────────────────────────────────────────────
2044
1991
  const userFlowSection = session.userFlowResults.length
2045
1992
  ? session.userFlowResults.map(f => `
2046
1993
  <div class="card" style="margin-bottom:1rem">
@@ -2057,7 +2004,6 @@ function buildHTMLReport(session) {
2057
2004
  </div>`).join('')
2058
2005
  : '<p class="no-data">No user flow simulations</p>';
2059
2006
 
2060
- // ── NEW v15: Memory section ───────────────────────────────────────────────
2061
2007
  const memorySection = session.memorySnapshots.length
2062
2008
  ? session.memorySnapshots.map(m => `
2063
2009
  <div class="card" style="margin-bottom:1rem">
@@ -2068,11 +2014,10 @@ function buildHTMLReport(session) {
2068
2014
  ${m.snapshots[0] ? vitalCard('Init Heap', Math.round(m.snapshots[0].used/1024/1024), 50, 'MB') : ''}
2069
2015
  ${m.snapshots[1] ? vitalCard('After Nav', Math.round(m.snapshots[1].used/1024/1024), 60, 'MB') : ''}
2070
2016
  </div>
2071
- ${m.hasLeak ? `<div class="bug-rec" style="margin-top:.75rem">⚠️ Possible memory leak: +${m.growthMB}MB after navigation. Check for event listener leaks and detached DOM nodes.</div>` : '<p style="color:#22c55e;margin-top:.75rem">✓ No significant memory growth detected</p>'}
2017
+ ${m.hasLeak ? `<div class="bug-rec" style="margin-top:.75rem">⚠️ Possible memory leak: +${m.growthMB}MB after navigation.</div>` : '<p style="color:#22c55e;margin-top:.75rem">✓ No significant memory growth detected</p>'}
2072
2018
  </div>`).join('')
2073
2019
  : '<p class="no-data">No memory tests (Playwright required)</p>';
2074
2020
 
2075
- // ── NEW v15: Dark mode section ────────────────────────────────────────────
2076
2021
  const darkModeSection = session.darkModeResults.length
2077
2022
  ? session.darkModeResults.map(dm => `
2078
2023
  <div class="card" style="margin-bottom:1rem">
@@ -2082,11 +2027,10 @@ function buildHTMLReport(session) {
2082
2027
  <div class="mc"><div class="ml">Media Query Found</div><div class="mv" style="font-size:1.2rem;color:${dm.hasMediaQuery?'#22c55e':'#64748b'}">${dm.hasMediaQuery ? '✓' : '–'}</div></div>
2083
2028
  <div class="mc"><div class="ml">Background Changes</div><div class="mv" style="font-size:1.2rem;color:${dm.differentFromLight?'#22c55e':'#64748b'}">${dm.differentFromLight ? '✓' : '–'}</div></div>
2084
2029
  </div>
2085
- ${!dm.supportsDark ? `<div class="bug-rec">💡 Consider adding dark mode with <code>@media (prefers-color-scheme: dark)</code></div>` : ''}
2030
+ ${!dm.supportsDark ? `<div class="bug-rec">💡 Add dark mode with <code>@media (prefers-color-scheme: dark)</code></div>` : ''}
2086
2031
  </div>`).join('')
2087
2032
  : '<p class="no-data">No dark mode tests (Playwright required)</p>';
2088
2033
 
2089
- // ── NEW v15: Redirect chains section ─────────────────────────────────────
2090
2034
  const redirectSection = session.redirectChains.length
2091
2035
  ? session.redirectChains.map(rc => `
2092
2036
  <div class="card" style="margin-bottom:1rem">
@@ -2104,7 +2048,6 @@ function buildHTMLReport(session) {
2104
2048
  </div>`).join('')
2105
2049
  : '<p class="no-data">No redirect chains analyzed</p>';
2106
2050
 
2107
- // ── Form tests section ────────────────────────────────────────────────────
2108
2051
  const formTestSection = session.formTests.length
2109
2052
  ? session.formTests.map(f => `
2110
2053
  <div class="card" style="margin-bottom:1rem">
@@ -2119,7 +2062,6 @@ function buildHTMLReport(session) {
2119
2062
  </div>`).join('')
2120
2063
  : '<p class="no-data">No forms found or Playwright not available</p>';
2121
2064
 
2122
- // ── Cache headers section ─────────────────────────────────────────────────
2123
2065
  const cacheSection = session.cacheHeaders.length
2124
2066
  ? `<table>
2125
2067
  <thead><tr><th>URL</th><th>Cache-Control</th><th>ETag</th><th>Max-Age</th><th>CDN Cache</th><th>Status</th></tr></thead>
@@ -2134,7 +2076,6 @@ function buildHTMLReport(session) {
2134
2076
  </table>`
2135
2077
  : '<p class="no-data">No cache audits</p>';
2136
2078
 
2137
- // ── Error pages section ───────────────────────────────────────────────────
2138
2079
  const errorPageSection = session.errorPageTests.length
2139
2080
  ? `<table>
2140
2081
  <thead><tr><th>Test</th><th>Actual Status</th><th>Custom Page</th><th>Status</th></tr></thead>
@@ -2147,7 +2088,6 @@ function buildHTMLReport(session) {
2147
2088
  </table>`
2148
2089
  : '<p class="no-data">No error page tests</p>';
2149
2090
 
2150
- // ── Console errors table ──────────────────────────────────────────────────
2151
2091
  const consoleSection = session.consoleErrors.length
2152
2092
  ? `<table>
2153
2093
  <thead><tr><th>Type</th><th>Message</th><th>URL</th></tr></thead>
@@ -2159,7 +2099,6 @@ function buildHTMLReport(session) {
2159
2099
  </table>`
2160
2100
  : '<p class="no-data">No console errors 🎉</p>';
2161
2101
 
2162
- // ── Network failures table ────────────────────────────────────────────────
2163
2102
  const networkSection = session.networkLog.length
2164
2103
  ? `<table>
2165
2104
  <thead><tr><th>URL</th><th>Method</th><th>Failure</th></tr></thead>
@@ -2171,7 +2110,6 @@ function buildHTMLReport(session) {
2171
2110
  </table>`
2172
2111
  : '<p class="no-data">No network failures 🎉</p>';
2173
2112
 
2174
- // Mixed content
2175
2113
  const mixedContentSection = session.mixedContentIssues.length
2176
2114
  ? `<table>
2177
2115
  <thead><tr><th>URL</th><th>Issue</th></tr></thead>
@@ -2201,7 +2139,7 @@ function buildHTMLReport(session) {
2201
2139
  <head>
2202
2140
  <meta charset="UTF-8">
2203
2141
  <meta name="viewport" content="width=device-width,initial-scale=1">
2204
- <title>Backlist QA v15 Report — ${esc(session.id)}</title>
2142
+ <title>Backlist QA v${VERSION} Report — ${esc(session.id)}</title>
2205
2143
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
2206
2144
  <style>
2207
2145
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap');
@@ -2309,7 +2247,7 @@ footer{text-align:center;color:var(--dim);font-size:.68rem;padding:2rem;border-t
2309
2247
  <body>
2310
2248
  <header>
2311
2249
  <div>
2312
- <div class="logo">⚡ Backlist Enterprise QA v15</div>
2250
+ <div class="logo">⚡ Backlist Enterprise QA v${VERSION}</div>
2313
2251
  <div class="header-meta">
2314
2252
  Run: ${esc(session.id)} · ${new Date(session.startedAt).toLocaleString()} · ${formatDuration(summary.duration)} · v${VERSION}
2315
2253
  </div>
@@ -2349,7 +2287,6 @@ footer{text-align:center;color:var(--dim);font-size:.68rem;padding:2rem;border-t
2349
2287
  ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Browser Mode</strong> — Screenshots, Web Vitals, DOM tests, Interactions, User Flow, Memory, Dark Mode, All Viewports</div>' : ''}
2350
2288
  <div class="real-banner">✅ <strong>100% Real Runtime Data</strong> — All results from actual HTTP requests and live Chromium browser testing.</div>
2351
2289
 
2352
- <!-- OVERVIEW -->
2353
2290
  <div id="tab-overview" class="tab-panel active">
2354
2291
  ${urlsStr ? `<div class="card"><div class="card-title">Target URLs</div>${urlsStr}</div>` : ''}
2355
2292
  <div class="metrics">
@@ -2376,7 +2313,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2376
2313
  </div>
2377
2314
  </div>
2378
2315
 
2379
- <!-- SCREENSHOTS -->
2380
2316
  <div id="tab-screenshots" class="tab-panel">
2381
2317
  <div class="card">
2382
2318
  <div class="card-title">Browser Screenshots <span>${session.screenshots.length} captured (${vpCount} viewports + dark mode)</span></div>
@@ -2384,7 +2320,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2384
2320
  </div>
2385
2321
  </div>
2386
2322
 
2387
- <!-- VIEWPORTS -->
2388
2323
  <div id="tab-viewports" class="tab-panel">
2389
2324
  <div class="card">
2390
2325
  <div class="card-title">Multi-Viewport Testing <span>${vpCount} viewports</span></div>
@@ -2392,7 +2327,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2392
2327
  </div>
2393
2328
  </div>
2394
2329
 
2395
- <!-- TESTS -->
2396
2330
  <div id="tab-tests" class="tab-panel">
2397
2331
  <div class="search-bar">
2398
2332
  <input type="text" id="testSearch" placeholder="Search tests..." onkeyup="filterTests()">
@@ -2415,7 +2349,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2415
2349
  </div>
2416
2350
  </div>
2417
2351
 
2418
- <!-- BUGS -->
2419
2352
  <div id="tab-bugs" class="tab-panel">
2420
2353
  <div class="search-bar">
2421
2354
  <input type="text" id="bugSearch" placeholder="Search bugs..." onkeyup="filterBugs()">
@@ -2432,7 +2365,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2432
2365
  <div id="bugList">${bugCards}</div>
2433
2366
  </div>
2434
2367
 
2435
- <!-- ROUTES -->
2436
2368
  <div id="tab-routes" class="tab-panel">
2437
2369
  <div class="card">
2438
2370
  <div class="card-title">Discovered Routes <span>${session.routeMap.length} pages/APIs</span></div>
@@ -2443,7 +2375,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2443
2375
  </div>
2444
2376
  </div>
2445
2377
 
2446
- <!-- SECURITY -->
2447
2378
  <div id="tab-security" class="tab-panel">
2448
2379
  <div class="card">
2449
2380
  <div class="card-title">Security Scan <span>${session.secFindings.length} checks · ${session.secFindings.filter(f=>!f.pass).length} issues</span></div>
@@ -2454,73 +2385,61 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2454
2385
  </div>
2455
2386
  </div>
2456
2387
 
2457
- <!-- PERFORMANCE -->
2458
2388
  <div id="tab-performance" class="tab-panel">
2459
2389
  <div class="card-title" style="padding:.5rem 0 1rem">Performance — Real Web Vitals (Playwright Chromium)</div>
2460
2390
  ${perfSection}
2461
2391
  </div>
2462
2392
 
2463
- <!-- LOAD TEST -->
2464
2393
  <div id="tab-loadtest" class="tab-panel">
2465
2394
  <div class="card-title" style="padding:.5rem 0 1rem">Load Testing — Concurrent Requests</div>
2466
2395
  ${loadTestSection}
2467
2396
  </div>
2468
2397
 
2469
- <!-- A11Y -->
2470
2398
  <div id="tab-a11y" class="tab-panel">
2471
2399
  <div class="card-title" style="padding:.5rem 0 1rem">Accessibility — WCAG 2.1 HTML Analysis (15 rules)</div>
2472
2400
  ${a11ySection}
2473
2401
  </div>
2474
2402
 
2475
- <!-- SEO -->
2476
2403
  <div id="tab-seo" class="tab-panel">
2477
2404
  <div class="card-title" style="padding:.5rem 0 1rem">SEO Analysis — Googlebot User-Agent (21 checks)</div>
2478
2405
  ${seoSection}
2479
2406
  </div>
2480
2407
 
2481
- <!-- DARK MODE -->
2482
2408
  <div id="tab-darkmode" class="tab-panel">
2483
2409
  <div class="card-title" style="padding:.5rem 0 1rem">Dark Mode Testing</div>
2484
2410
  ${darkModeSection}
2485
2411
  </div>
2486
2412
 
2487
- <!-- USER FLOW -->
2488
2413
  <div id="tab-userflow" class="tab-panel">
2489
2414
  <div class="card-title" style="padding:.5rem 0 1rem">User Flow Simulation</div>
2490
2415
  ${userFlowSection}
2491
2416
  </div>
2492
2417
 
2493
- <!-- FORMS -->
2494
2418
  <div id="tab-forms" class="tab-panel">
2495
2419
  <div class="card-title" style="padding:.5rem 0 1rem">Form Testing</div>
2496
2420
  ${formTestSection}
2497
2421
  </div>
2498
2422
 
2499
- <!-- COOKIES -->
2500
2423
  <div id="tab-cookies" class="tab-panel">
2501
2424
  <div class="card-title" style="padding:.5rem 0 1rem">Cookie Security Audit</div>
2502
2425
  ${cookieSection}
2503
2426
  </div>
2504
2427
 
2505
- <!-- MEMORY -->
2506
2428
  <div id="tab-memory" class="tab-panel">
2507
2429
  <div class="card-title" style="padding:.5rem 0 1rem">Memory Leak Detection</div>
2508
2430
  ${memorySection}
2509
2431
  </div>
2510
2432
 
2511
- <!-- BROKEN LINKS -->
2512
2433
  <div id="tab-brokenlinks" class="tab-panel">
2513
2434
  <div class="card-title" style="padding:.5rem 0 1rem">Broken Link Scanner</div>
2514
2435
  ${brokenLinksSection}
2515
2436
  </div>
2516
2437
 
2517
- <!-- REDIRECTS -->
2518
2438
  <div id="tab-redirects" class="tab-panel">
2519
2439
  <div class="card-title" style="padding:.5rem 0 1rem">Redirect Chain Analysis</div>
2520
2440
  ${redirectSection}
2521
2441
  </div>
2522
2442
 
2523
- <!-- THIRD PARTY -->
2524
2443
  <div id="tab-thirdparty" class="tab-panel">
2525
2444
  <div class="card">
2526
2445
  <div class="card-title">Third-Party Script Audit <span>${session.thirdPartyScripts.length} external scripts</span></div>
@@ -2528,7 +2447,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2528
2447
  </div>
2529
2448
  </div>
2530
2449
 
2531
- <!-- CACHE -->
2532
2450
  <div id="tab-cache" class="tab-panel">
2533
2451
  <div class="card">
2534
2452
  <div class="card-title">Cache Headers Audit</div>
@@ -2536,7 +2454,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2536
2454
  </div>
2537
2455
  </div>
2538
2456
 
2539
- <!-- ERROR PAGES -->
2540
2457
  <div id="tab-errorpages" class="tab-panel">
2541
2458
  <div class="card">
2542
2459
  <div class="card-title">Error Page Testing</div>
@@ -2544,7 +2461,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2544
2461
  </div>
2545
2462
  </div>
2546
2463
 
2547
- <!-- MIXED CONTENT -->
2548
2464
  <div id="tab-mixed" class="tab-panel">
2549
2465
  <div class="card">
2550
2466
  <div class="card-title">Mixed Content Issues</div>
@@ -2552,7 +2468,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2552
2468
  </div>
2553
2469
  </div>
2554
2470
 
2555
- <!-- CONSOLE -->
2556
2471
  <div id="tab-console" class="tab-panel">
2557
2472
  <div class="card">
2558
2473
  <div class="card-title">Console Errors &amp; Warnings <span>${session.consoleErrors.length} entries</span></div>
@@ -2560,7 +2475,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2560
2475
  </div>
2561
2476
  </div>
2562
2477
 
2563
- <!-- NETWORK -->
2564
2478
  <div id="tab-network" class="tab-panel">
2565
2479
  <div class="card">
2566
2480
  <div class="card-title">Network Failures <span>${session.networkLog.length} failures</span></div>
@@ -2626,7 +2540,7 @@ async function runQAEngine(session, opts = {}) {
2626
2540
  };
2627
2541
 
2628
2542
  try {
2629
- // ── Phase 1: Discovery ───────────────────────────────────────────────
2543
+ // Phase 1: Discovery
2630
2544
  dash.setPhase('🔍 Phase 1: Route Discovery & Crawling');
2631
2545
  for (const [label, url] of Object.entries(session.urls)) {
2632
2546
  if (!url) continue;
@@ -2644,7 +2558,7 @@ async function runQAEngine(session, opts = {}) {
2644
2558
  message: `Discovered ${routes.length} routes in ${Date.now()-t0}ms`, url, label });
2645
2559
  }
2646
2560
 
2647
- // ── Phase 2: Redirect Chain Analysis (v15) ───────────────────────────
2561
+ // Phase 2: Redirect Chain Analysis
2648
2562
  dash.setPhase('↪ Phase 2: Redirect Chain Analysis');
2649
2563
  for (const [label, url] of Object.entries(session.urls)) {
2650
2564
  if (!url) continue;
@@ -2660,7 +2574,7 @@ async function runQAEngine(session, opts = {}) {
2660
2574
  }
2661
2575
  }
2662
2576
 
2663
- // ── Phase 3: Playwright Real Browser ────────────────────────────────
2577
+ // Phase 3: Playwright Real Browser
2664
2578
  dash.setPhase('🎭 Phase 3: Playwright Real Browser Tests');
2665
2579
  const chromium = await getPlaywright();
2666
2580
 
@@ -2698,7 +2612,6 @@ async function runQAEngine(session, opts = {}) {
2698
2612
  if (!i.pass) session.addBug({ title: `Interaction Failed: ${i.name}`, severity: 'P2', type: 'javascript', url, evidence: { value: i.value } });
2699
2613
  }
2700
2614
 
2701
- // Viewport results
2702
2615
  for (const [vk, vp] of Object.entries(pw.viewportResults || {})) {
2703
2616
  if (!vp.error) {
2704
2617
  addResult({ name: `Viewport: ${vp.label}`, type: 'viewport',
@@ -2707,7 +2620,6 @@ async function runQAEngine(session, opts = {}) {
2707
2620
  }
2708
2621
  }
2709
2622
 
2710
- // User flow
2711
2623
  if (pw.userFlow?.steps) {
2712
2624
  for (const step of pw.userFlow.steps) {
2713
2625
  addResult({ name: `Flow: ${step.name}`, type: 'user-flow',
@@ -2715,14 +2627,12 @@ async function runQAEngine(session, opts = {}) {
2715
2627
  }
2716
2628
  }
2717
2629
 
2718
- // Dark mode
2719
2630
  if (pw.darkMode && !pw.darkMode.error) {
2720
2631
  addResult({ name: `[${label}] Dark Mode Support`, type: 'dark-mode',
2721
2632
  status: pw.darkMode.supportsDark ? 'PASS' : 'FAIL',
2722
2633
  message: pw.darkMode.supportsDark ? 'prefers-color-scheme supported' : 'No dark mode support', url, label });
2723
2634
  }
2724
2635
 
2725
- // Memory
2726
2636
  if (pw.memoryLeak) {
2727
2637
  addResult({ name: `[${label}] Memory Leak Check`, type: 'memory',
2728
2638
  status: pw.memoryLeak.hasLeak ? 'FAIL' : 'PASS',
@@ -2730,39 +2640,33 @@ async function runQAEngine(session, opts = {}) {
2730
2640
  if (pw.memoryLeak.hasLeak) session.addBug({ title: `Memory leak: +${pw.memoryLeak.growthMB}MB`, severity: pw.memoryLeak.severity, type: 'performance', url, evidence: pw.memoryLeak });
2731
2641
  }
2732
2642
 
2733
- // Form tests
2734
2643
  for (const f of pw.forms || []) {
2735
2644
  addResult({ name: `Form #${f.formIndex+1}: ${f.action||'self'}`, type: 'form',
2736
2645
  status: f.passed ? 'PASS' : 'FAIL', message: (f.issues||[]).join(', ') || 'OK', url, label });
2737
2646
  }
2738
2647
 
2739
- // Third-party
2740
2648
  if (pw.thirdParty?.length > 0) {
2741
2649
  addResult({ name: `[${label}] Third-party scripts`, type: 'third-party',
2742
2650
  status: 'PASS', message: `${pw.thirdParty.length} external scripts: ${pw.thirdParty.map(t=>t.vendor).join(', ')}`, url, label });
2743
2651
  }
2744
2652
 
2745
- // JS errors
2746
2653
  for (const err of pw.jsErrors || []) {
2747
2654
  addResult({ name: `JS Error: ${err.message?.slice(0,60)}`, type: 'javascript',
2748
2655
  status: 'FAIL', message: err.message, url, label, severity: 'P2' });
2749
2656
  session.addBug({ title: `JS Error: ${err.message?.slice(0,80)}`, severity: 'P2', type: 'javascript', url, evidence: { message: err.message } });
2750
2657
  }
2751
2658
 
2752
- // Network failures
2753
2659
  for (const fail of pw.networkFails || []) {
2754
2660
  addResult({ name: `Network Fail: ${fail.url?.split('/').pop()?.slice(0,40)}`, type: 'network',
2755
2661
  status: 'FAIL', message: fail.failure || `HTTP ${fail.status}`, url: fail.url, label });
2756
2662
  session.addBug({ title: `Network Failure: ${fail.url?.split('/').pop()}`, severity: fail.status >= 500 ? 'P1' : 'P2', type: 'network', url: fail.url });
2757
2663
  }
2758
2664
 
2759
- // Mixed content
2760
2665
  for (const mc of pw.mixedContent || []) {
2761
2666
  addResult({ name: `Mixed Content`, type: 'security', status: 'FAIL', message: mc, url, label });
2762
2667
  session.addBug({ title: `Mixed Content detected`, severity: 'P1', type: 'security', url, evidence: { text: mc } });
2763
2668
  }
2764
2669
 
2765
- // Web Vitals
2766
2670
  const { lcp, fcp, cls, tbt, ttfb } = pw.vitals || {};
2767
2671
  const vitalTests = [
2768
2672
  { name: 'TTFB', val: ttfb || pw.vitals?.ttfb, threshold: 800 },
@@ -2807,7 +2711,7 @@ async function runQAEngine(session, opts = {}) {
2807
2711
  }
2808
2712
  }
2809
2713
 
2810
- // ── Phase 4: API Validation ──────────────────────────────────────────
2714
+ // Phase 4: API Validation
2811
2715
  dash.setPhase('📡 Phase 4: API Validation & Contract Testing');
2812
2716
  const apiRoutes = session.routeMap.filter(r => r.type === 'api' || r.url?.includes('/api/'));
2813
2717
  dash.log(`Validating ${apiRoutes.length} API endpoints...`);
@@ -2824,7 +2728,7 @@ async function runQAEngine(session, opts = {}) {
2824
2728
  description: contract.issues.join(', '), evidence: { status: contract.status, issues: contract.issues } });
2825
2729
  }
2826
2730
 
2827
- // ── Phase 5: Security ────────────────────────────────────────────────
2731
+ // Phase 5: Security
2828
2732
  dash.setPhase('🛡️ Phase 5: Security Scan (20+ checks)');
2829
2733
  for (const [label, url] of Object.entries(session.urls)) {
2830
2734
  if (!url) continue;
@@ -2841,7 +2745,7 @@ async function runQAEngine(session, opts = {}) {
2841
2745
  }
2842
2746
  }
2843
2747
 
2844
- // ── Phase 6: Cookie Audit (v15) ──────────────────────────────────────
2748
+ // Phase 6: Cookie Audit
2845
2749
  dash.setPhase('🍪 Phase 6: Cookie Security Audit');
2846
2750
  for (const [label, url] of Object.entries(session.urls)) {
2847
2751
  if (!url) continue;
@@ -2858,7 +2762,7 @@ async function runQAEngine(session, opts = {}) {
2858
2762
  }
2859
2763
  }
2860
2764
 
2861
- // ── Phase 7: Cache Headers (v15) ─────────────────────────────────────
2765
+ // Phase 7: Cache Headers
2862
2766
  dash.setPhase('💾 Phase 7: Cache Headers Audit');
2863
2767
  for (const [label, url] of Object.entries(session.urls)) {
2864
2768
  if (!url) continue;
@@ -2869,7 +2773,7 @@ async function runQAEngine(session, opts = {}) {
2869
2773
  message: cacheResult.cacheControl || 'No cache headers', url, label });
2870
2774
  }
2871
2775
 
2872
- // ── Phase 8: Broken Links (v15) ──────────────────────────────────────
2776
+ // Phase 8: Broken Links
2873
2777
  dash.setPhase('🔗 Phase 8: Broken Link Scanner');
2874
2778
  const pageRoutes8 = session.routeMap.filter(r => r.type === 'page').slice(0, 5);
2875
2779
  for (const route of pageRoutes8) {
@@ -2885,7 +2789,7 @@ async function runQAEngine(session, opts = {}) {
2885
2789
  }
2886
2790
  }
2887
2791
 
2888
- // ── Phase 9: Error Pages (v15) ───────────────────────────────────────
2792
+ // Phase 9: Error Pages
2889
2793
  dash.setPhase('🚫 Phase 9: Error Page Testing');
2890
2794
  for (const [label, url] of Object.entries(session.urls)) {
2891
2795
  if (!url) continue;
@@ -2898,7 +2802,7 @@ async function runQAEngine(session, opts = {}) {
2898
2802
  }
2899
2803
  }
2900
2804
 
2901
- // ── Phase 10: Accessibility ──────────────────────────────────────────
2805
+ // Phase 10: Accessibility
2902
2806
  dash.setPhase('♿ Phase 10: Accessibility Check (WCAG 2.1)');
2903
2807
  const pageRoutes10 = session.routeMap.filter(r => r.type === 'page' || r.type === 'auth').slice(0, 10);
2904
2808
  for (const route of pageRoutes10) {
@@ -2915,7 +2819,7 @@ async function runQAEngine(session, opts = {}) {
2915
2819
  }
2916
2820
  }
2917
2821
 
2918
- // ── Phase 11: SEO ────────────────────────────────────────────────────
2822
+ // Phase 11: SEO
2919
2823
  dash.setPhase('🔎 Phase 11: SEO Validation (21 checks)');
2920
2824
  const seoRoutes = session.routeMap.filter(r => r.type === 'page').slice(0, 10);
2921
2825
  for (const route of seoRoutes) {
@@ -2929,7 +2833,7 @@ async function runQAEngine(session, opts = {}) {
2929
2833
  }
2930
2834
  }
2931
2835
 
2932
- // ── Phase 12: Load Test (v15) ────────────────────────────────────────
2836
+ // Phase 12: Load Test
2933
2837
  if (opts.loadTest !== false) {
2934
2838
  dash.setPhase('🔥 Phase 12: Load Testing');
2935
2839
  for (const [label, url] of Object.entries(session.urls)) {
@@ -2946,7 +2850,7 @@ async function runQAEngine(session, opts = {}) {
2946
2850
  }
2947
2851
  }
2948
2852
 
2949
- // ── Phase 13: HTTP Version (v15) ─────────────────────────────────────
2853
+ // Phase 13: HTTP Version
2950
2854
  dash.setPhase('🌐 Phase 13: HTTP Version & Protocol Check');
2951
2855
  for (const [label, url] of Object.entries(session.urls)) {
2952
2856
  if (!url) continue;
@@ -2956,7 +2860,7 @@ async function runQAEngine(session, opts = {}) {
2956
2860
  status: http.isHTTPS ? 'PASS' : 'FAIL', message: http.isHTTPS ? 'HTTPS in use' : 'HTTP only', url, label });
2957
2861
  }
2958
2862
 
2959
- // ── Phase 14: AI Classification ──────────────────────────────────────
2863
+ // Phase 14: AI Classification
2960
2864
  dash.setPhase('🤖 Phase 14: AI Bug Classification');
2961
2865
  dash.log(`Classifying ${session.bugs.length} bugs...`);
2962
2866
  for (const bug of session.bugs) {
@@ -3045,7 +2949,7 @@ async function saveToHistory(session, htmlPath, jsonPath) {
3045
2949
  }
3046
2950
 
3047
2951
  // ═══════════════════════════════════════════════════════════════════════════
3048
- // Public API — runUrlQA (main entry point)
2952
+ // Public API
3049
2953
  // ═══════════════════════════════════════════════════════════════════════════
3050
2954
  export async function runUrlQA({ localUrl, stagingUrl, prodUrl, loadTest = true } = {}) {
3051
2955
  const urls = {};