create-backlist 10.1.3 → 10.1.6

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.
@@ -1,10 +1,11 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════════
2
- // Backlist Enterprise QA Engine v15.0 — ULTRA LIVE TESTING EDITION
2
+ // Backlist Enterprise QA Engine v15.1 — ULTRA LIVE TESTING EDITION
3
3
  // ✅ Real Playwright Browser · ✅ AI Bug Classifier · ✅ Live WebSocket Monitor
4
4
  // ✅ Visual Regression · ✅ API Contract Testing · ✅ Real User Simulation
5
5
  // ✅ Cookie/Auth Testing · ✅ Dark Mode Testing · ✅ Multi-viewport Testing
6
6
  // ✅ Memory Leak Detection · ✅ Load Testing · ✅ WebSocket Testing
7
7
  // ✅ Broken Link Scanner · ✅ Font/Asset Audit · ✅ Rich HTML Reports v15
8
+ // ✅ QA Ecosystem Registry (60+ tools across 10 categories)
8
9
  // ═══════════════════════════════════════════════════════════════════════════
9
10
 
10
11
  import * as p from '@clack/prompts';
@@ -18,7 +19,7 @@ import { performance } from 'node:perf_hooks';
18
19
  import { EventEmitter } from 'node:events';
19
20
 
20
21
  // ── Constants ─────────────────────────────────────────────────────────────
21
- export const VERSION = '15.0.0';
22
+ export const VERSION = '15.1.0';
22
23
  export const QA_DIR = path.join(process.cwd(), '.BACKLIST', 'qa');
23
24
  export const REPORT_DIR = path.join(QA_DIR, 'reports');
24
25
  export const HISTORY_FILE = path.join(QA_DIR, 'history.json');
@@ -36,6 +37,134 @@ export const VIEWPORTS = {
36
37
  mobile_sm : { width: 320, height: 568, label: 'Mobile (small)' },
37
38
  };
38
39
 
40
+ // ── QA Ecosystem Tool Registry ────────────────────────────────────────────
41
+ // Comprehensive catalog of 60+ QA tools across 10 categories
42
+ // ──────────────────────────────────────────────────────────────────────────
43
+ export const QA_ECOSYSTEM = {
44
+ // ─── Browser / E2E Automation ──────────────────────────────────────────
45
+ browserAutomation: {
46
+ label: 'Browser / E2E Automation',
47
+ icon: '🌐',
48
+ tools: [
49
+ { name: 'Playwright', pkg: 'playwright', description: 'Cross-browser E2E testing with auto-wait and codegen', type: 'npm', install: 'npm i -D playwright && npx playwright install', free: true, url: 'https://playwright.dev' },
50
+ { name: 'Cypress', pkg: 'cypress', description: 'Fast, reliable E2E testing with time-travel debugging', type: 'npm', install: 'npm i -D cypress', free: true, url: 'https://www.cypress.io' },
51
+ { name: 'Selenium WebDriver', pkg: 'selenium-webdriver', description: 'Industry-standard browser automation across all major browsers', type: 'npm', install: 'npm i -D selenium-webdriver', free: true, url: 'https://www.selenium.dev' },
52
+ { name: 'WebdriverIO', pkg: 'webdriverio', description: 'Next-gen browser/mobile automation framework for Node.js', type: 'npm', install: 'npm i -D webdriverio @wdio/cli', free: true, url: 'https://webdriver.io' },
53
+ { name: 'TestCafe', pkg: 'testcafe', description: 'E2E testing without WebDriver — works across all browsers', type: 'npm', install: 'npm i -D testcafe', free: true, url: 'https://testcafe.io' },
54
+ { name: 'Nightwatch.js', pkg: 'nightwatch', description: 'Integrated E2E testing with Selenium/WebDriver built-in', type: 'npm', install: 'npm i -D nightwatch', free: true, url: 'https://nightwatchjs.org' },
55
+ { name: 'CodeceptJS', pkg: 'codeceptjs', description: 'Scenario-driven E2E testing with multiple backend support', type: 'npm', install: 'npm i -D codeceptjs', free: true, url: 'https://codecept.io' },
56
+ { name: 'Puppeteer', pkg: 'puppeteer', description: 'Chrome/Chromium automation with high-level API by Google', type: 'npm', install: 'npm i -D puppeteer', free: true, url: 'https://pptr.dev' },
57
+ { name: 'Protractor', pkg: 'protractor', description: 'Angular E2E testing framework (legacy, deprecated)', type: 'npm', install: 'npm i -D protractor', free: true, url: 'https://www.protractortest.org', deprecated: true },
58
+ { name: 'Appium', pkg: 'appium', description: 'Cross-platform mobile + web automation framework', type: 'npm', install: 'npm i -D appium', free: true, url: 'https://appium.io' },
59
+ ],
60
+ },
61
+
62
+ // ─── JavaScript / Node.js Unit + Integration Frameworks ────────────────
63
+ unitIntegration: {
64
+ label: 'JS / Node.js Test Frameworks (Unit + Integration)',
65
+ icon: '⚙️',
66
+ tools: [
67
+ { name: 'Jest', pkg: 'jest', description: 'Delightful JavaScript testing with zero config, snapshots, and mocking', type: 'npm', install: 'npm i -D jest', free: true, url: 'https://jestjs.io' },
68
+ { name: 'Mocha', pkg: 'mocha', description: 'Flexible test framework with rich reporting and async support', type: 'npm', install: 'npm i -D mocha', free: true, url: 'https://mochajs.org' },
69
+ { name: 'Vitest', pkg: 'vitest', description: 'Blazing fast Vite-native unit testing with Jest-compatible API', type: 'npm', install: 'npm i -D vitest', free: true, url: 'https://vitest.dev' },
70
+ { name: 'Jasmine', pkg: 'jasmine', description: 'Behavior-driven testing framework — no DOM or framework dependencies', type: 'npm', install: 'npm i -D jasmine', free: true, url: 'https://jasmine.github.io' },
71
+ { name: 'QUnit', pkg: 'qunit', description: 'Powerful, easy-to-use JavaScript unit testing framework', type: 'npm', install: 'npm i -D qunit', free: true, url: 'https://qunitjs.com' },
72
+ ],
73
+ },
74
+
75
+ // ─── API Testing ───────────────────────────────────────────────────────
76
+ apiTesting: {
77
+ label: 'API Testing (npm tools)',
78
+ icon: '🔌',
79
+ tools: [
80
+ { name: 'Supertest', pkg: 'supertest', description: 'HTTP assertion library for testing Node.js HTTP servers', type: 'npm', install: 'npm i -D supertest', free: true, url: 'https://github.com/ladjs/supertest' },
81
+ { name: 'Axios', pkg: 'axios', description: 'Promise-based HTTP client for browser and Node.js (with test runners)', type: 'npm', install: 'npm i -D axios', free: true, url: 'https://axios-http.com' },
82
+ { name: 'Newman', pkg: 'newman', description: 'Postman CLI collection runner for automated API testing', type: 'npm', install: 'npm i -g newman', free: true, url: 'https://github.com/postmanlabs/newman' },
83
+ { name: 'Pact', pkg: '@pact-foundation/pact', description: 'Consumer-driven contract testing for microservices', type: 'npm', install: 'npm i -D @pact-foundation/pact', free: true, url: 'https://pact.io' },
84
+ { name: 'Frisby.js', pkg: 'frisby', description: 'REST API testing framework built on Jest for easy assertions', type: 'npm', install: 'npm i -D frisby', free: true, url: 'https://frisbyjs.com' },
85
+ ],
86
+ },
87
+
88
+ // ─── BDD / Cucumber Style Testing ──────────────────────────────────────
89
+ bddCucumber: {
90
+ label: 'BDD / Cucumber Style Testing',
91
+ icon: '🧰',
92
+ tools: [
93
+ { name: 'Cucumber.js', pkg: '@cucumber/cucumber', description: 'Official Cucumber BDD framework for JavaScript', type: 'npm', install: 'npm i -D @cucumber/cucumber', free: true, url: 'https://cucumber.io' },
94
+ { name: 'CodeceptJS BDD', pkg: 'codeceptjs', description: 'CodeceptJS in BDD mode with Gherkin syntax support', type: 'npm', install: 'npm i -D codeceptjs', free: true, url: 'https://codecept.io/bdd' },
95
+ { name: 'Jest-Cucumber', pkg: 'jest-cucumber', description: 'Gherkin-style BDD testing that executes through Jest', type: 'npm', install: 'npm i -D jest-cucumber', free: true, url: 'https://github.com/bencompton/jest-cucumber' },
96
+ ],
97
+ },
98
+
99
+ // ─── Mocking / Stubbing ────────────────────────────────────────────────
100
+ mockingStubbing: {
101
+ label: 'Mocking / Stubbing (for QA automation)',
102
+ icon: '🧪',
103
+ tools: [
104
+ { name: 'Sinon.js', pkg: 'sinon', description: 'Standalone spies, stubs, and mocks for JavaScript', type: 'npm', install: 'npm i -D sinon', free: true, url: 'https://sinonjs.org' },
105
+ { name: 'Nock', pkg: 'nock', description: 'HTTP server mocking and expectations library for Node.js', type: 'npm', install: 'npm i -D nock', free: true, url: 'https://github.com/nock/nock' },
106
+ { name: 'MSW (Mock Service Worker)', pkg: 'msw', description: 'API mocking at the network level — intercepts requests seamlessly', type: 'npm', install: 'npm i -D msw', free: true, url: 'https://mswjs.io' },
107
+ ],
108
+ },
109
+
110
+ // ─── Performance / Load Testing ────────────────────────────────────────
111
+ performanceLoad: {
112
+ label: 'Performance / Load Testing (free tools)',
113
+ icon: '📊',
114
+ tools: [
115
+ { name: 'k6', pkg: 'k6', description: 'Modern load testing tool with scripting in JavaScript', type: 'cli', install: 'brew install k6 || choco install k6', free: true, url: 'https://k6.io' },
116
+ { name: 'Artillery', pkg: 'artillery', description: 'Cloud-scale load testing with scenario scripting (YAML/JS)', type: 'npm', install: 'npm i -g artillery', free: true, url: 'https://www.artillery.io' },
117
+ { name: 'Autocannon', pkg: 'autocannon', description: 'Fast HTTP/1.1 benchmarking tool written in Node.js', type: 'npm', install: 'npm i -D autocannon', free: true, url: 'https://github.com/mcollina/autocannon' },
118
+ { name: 'Locust', pkg: 'locust', description: 'Python-based scalable load testing with real-time web UI', type: 'python', install: 'pip install locust', free: true, url: 'https://locust.io' },
119
+ ],
120
+ },
121
+
122
+ // ─── Mobile QA Automation ──────────────────────────────────────────────
123
+ mobileAutomation: {
124
+ label: 'Mobile QA Automation (free tools)',
125
+ icon: '📱',
126
+ tools: [
127
+ { name: 'Appium', pkg: 'appium', description: 'Cross-platform mobile automation (iOS, Android, Windows)', type: 'npm', install: 'npm i -g appium', free: true, url: 'https://appium.io' },
128
+ { name: 'Detox', pkg: 'detox', description: 'Gray-box E2E testing for React Native mobile apps', type: 'npm', install: 'npm i -D detox', free: true, url: 'https://wix.github.io/Detox' },
129
+ ],
130
+ },
131
+
132
+ // ─── Visual / Regression Testing ───────────────────────────────────────
133
+ visualRegression: {
134
+ label: 'Visual / Regression Testing (free or open-source core)',
135
+ icon: '🧱',
136
+ tools: [
137
+ { name: 'Percy', pkg: '@percy/cli', description: 'Visual testing with smart diffs and free-tier integrations', type: 'npm', install: 'npm i -D @percy/cli', free: true, url: 'https://percy.io' },
138
+ { name: 'Chromatic', pkg: 'chromatic', description: 'Storybook-based visual testing with change detection', type: 'npm', install: 'npm i -D chromatic', free: true, url: 'https://www.chromatic.com' },
139
+ { name: 'BackstopJS', pkg: 'backstopjs', description: 'Visual regression testing with headless Chrome screenshots', type: 'npm', install: 'npm i -D backstopjs', free: true, url: 'https://github.com/garris/BackstopJS' },
140
+ ],
141
+ },
142
+
143
+ // ─── Test Utilities / Helpers ──────────────────────────────────────────
144
+ testUtilities: {
145
+ label: 'Test Utilities / Helpers (npm)',
146
+ icon: '🧪',
147
+ tools: [
148
+ { name: 'React Testing Library', pkg: '@testing-library/react', description: 'Simple utilities for testing React components the way users use them', type: 'npm', install: 'npm i -D @testing-library/react', free: true, url: 'https://testing-library.com/react' },
149
+ { name: 'DOM Testing Library', pkg: '@testing-library/dom', description: 'Lightweight DOM testing utilities that encourage good practices', type: 'npm', install: 'npm i -D @testing-library/dom', free: true, url: 'https://testing-library.com' },
150
+ { name: 'Chai', pkg: 'chai', description: 'BDD / TDD assertion library with rich plugin ecosystem', type: 'npm', install: 'npm i -D chai', free: true, url: 'https://www.chaijs.com' },
151
+ { name: 'expect.js', pkg: 'expect.js', description: 'Minimalistic BDD-style assertion library', type: 'npm', install: 'npm i -D expect.js', free: true, url: 'https://github.com/Automattic/expect.js' },
152
+ { name: 'Enzyme', pkg: 'enzyme', description: 'React component testing utility (legacy)', type: 'npm', install: 'npm i -D enzyme', free: true, url: 'https://enzymejs.github.io/enzyme', deprecated: true },
153
+ ],
154
+ },
155
+
156
+ // ─── Local QA Infrastructure / Runners ─────────────────────────────────
157
+ infraRunners: {
158
+ label: 'Local QA Automation Runners / Helpers',
159
+ icon: '🧰',
160
+ tools: [
161
+ { name: 'Docker', pkg: 'docker', description: 'Containerized test environments for consistent reproducibility', type: 'docker', install: 'https://docs.docker.com/get-docker/', free: true, url: 'https://www.docker.com' },
162
+ { name: 'Testcontainers', pkg: 'testcontainers', description: 'Throwaway Docker containers for integration testing in Node.js', type: 'npm', install: 'npm i -D testcontainers', free: true, url: 'https://testcontainers.com' },
163
+ { name: 'LocalStack', pkg: 'localstack', description: 'Fully functional local AWS cloud stack for testing', type: 'python', install: 'pip install localstack || docker pull localstack/localstack', free: true, url: 'https://localstack.cloud' },
164
+ ],
165
+ },
166
+ };
167
+
39
168
  // ── Utilities ─────────────────────────────────────────────────────────────
40
169
  export const timestamp = () => new Date().toISOString();
41
170
  export const shortId = () => Math.random().toString(36).slice(2, 9);
@@ -76,7 +205,7 @@ async function getPlaywright() {
76
205
  }
77
206
 
78
207
  // ─────────────────────────────────────────────────────────────────────────
79
- // NEW v15: Image hash for visual regression
208
+ // Image hash for visual regression
80
209
  // ─────────────────────────────────────────────────────────────────────────
81
210
  function hashBuffer(buf) {
82
211
  return crypto.createHash('md5').update(buf).digest('hex');
@@ -101,30 +230,30 @@ export class QASession {
101
230
  a11yResults = [];
102
231
  seoResults = [];
103
232
  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 = [];
233
+ // v15 fields
234
+ visualRegressions = [];
235
+ cookieAudit = [];
236
+ loadTestResults = [];
237
+ brokenLinks = [];
238
+ fontAudit = [];
239
+ assetAudit = [];
240
+ memorySnapshots = [];
241
+ wsTests = [];
242
+ darkModeResults = [];
243
+ viewportResults = {};
244
+ apiContracts = [];
245
+ userFlowResults = [];
246
+ redirectChains = [];
118
247
  mixedContentIssues = [];
119
- cspViolations = [];
120
- thirdPartyScripts = [];
121
- errorPageTests = [];
122
- formTests = [];
123
- authTests = [];
124
- cacheHeaders = [];
125
- httpVersions = {};
126
- tlsInfo = {};
127
- dnsInfo = {};
248
+ cspViolations = [];
249
+ thirdPartyScripts = [];
250
+ errorPageTests = [];
251
+ formTests = [];
252
+ authTests = [];
253
+ cacheHeaders = [];
254
+ httpVersions = {};
255
+ tlsInfo = {};
256
+ dnsInfo = {};
128
257
 
129
258
  constructor(urls = {}) {
130
259
  this.id = `QA-${shortId().toUpperCase()}`;
@@ -156,7 +285,7 @@ export class QASession {
156
285
  }
157
286
 
158
287
  // ═══════════════════════════════════════════════════════════════════════════
159
- // HTTP Probe — real HTTP requests with v15 extras
288
+ // HTTP Probe — real HTTP requests
160
289
  // ═══════════════════════════════════════════════════════════════════════════
161
290
  async function httpProbe(url, { method = 'GET', timeout = 12000, headers = {}, body: reqBody = null, followRedirects = true } = {}) {
162
291
  const t0 = Date.now();
@@ -201,7 +330,7 @@ async function httpProbe(url, { method = 'GET', timeout = 12000, headers = {}, b
201
330
  }
202
331
 
203
332
  // ═══════════════════════════════════════════════════════════════════════════
204
- // NEW v15: Redirect Chain Analyzer
333
+ // Redirect Chain Analyzer
205
334
  // ═══════════════════════════════════════════════════════════════════════════
206
335
  async function analyzeRedirectChain(url) {
207
336
  const chain = [];
@@ -243,7 +372,7 @@ async function analyzeRedirectChain(url) {
243
372
  }
244
373
 
245
374
  // ═══════════════════════════════════════════════════════════════════════════
246
- // NEW v15: Load Test — concurrent requests
375
+ // Load Test — concurrent requests
247
376
  // ═══════════════════════════════════════════════════════════════════════════
248
377
  async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2000 } = {}) {
249
378
  const results = { requests: 0, errors: 0, timeouts: 0, responses: {} };
@@ -272,7 +401,7 @@ async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2
272
401
  } catch {
273
402
  results.errors++;
274
403
  }
275
- await sleep(50); // small breathing room
404
+ await sleep(50);
276
405
  }
277
406
  };
278
407
 
@@ -304,7 +433,7 @@ async function runLoadTest(url, { concurrency = 10, duration = 10000, rampUp = 2
304
433
  }
305
434
 
306
435
  // ═══════════════════════════════════════════════════════════════════════════
307
- // NEW v15: Cookie Audit
436
+ // Cookie Audit
308
437
  // ═══════════════════════════════════════════════════════════════════════════
309
438
  async function runCookieAudit(url) {
310
439
  const r = await httpProbe(url);
@@ -343,7 +472,7 @@ async function runCookieAudit(url) {
343
472
  }
344
473
 
345
474
  // ═══════════════════════════════════════════════════════════════════════════
346
- // NEW v15: Broken Link Scanner (deep)
475
+ // Broken Link Scanner (deep)
347
476
  // ═══════════════════════════════════════════════════════════════════════════
348
477
  async function scanBrokenLinks(url, { maxLinks = 100 } = {}) {
349
478
  const r = await httpProbe(url);
@@ -389,7 +518,7 @@ async function scanBrokenLinks(url, { maxLinks = 100 } = {}) {
389
518
  }
390
519
 
391
520
  // ═══════════════════════════════════════════════════════════════════════════
392
- // NEW v15: API Contract Tester
521
+ // API Contract Tester
393
522
  // ═══════════════════════════════════════════════════════════════════════════
394
523
  async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields = [], method = 'GET', body = null, headers = {} } = {}) {
395
524
  const r = await httpProbe(endpoint, { method, body, headers, timeout: 10000 });
@@ -400,7 +529,7 @@ async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields
400
529
  }
401
530
  if (r.parsed && expectedFields.length > 0) {
402
531
  for (const field of expectedFields) {
403
- const hasField = field.includes('.')
532
+ const hasField = field.includes('.')
404
533
  ? field.split('.').reduce((obj, k) => obj?.[k], r.parsed) !== undefined
405
534
  : r.parsed[field] !== undefined || (Array.isArray(r.parsed) && r.parsed[0]?.[field] !== undefined);
406
535
  if (!hasField) issues.push(`Missing field: ${field}`);
@@ -422,7 +551,7 @@ async function testAPIContract(endpoint, { expectedStatus = 200, expectedFields
422
551
  }
423
552
 
424
553
  // ═══════════════════════════════════════════════════════════════════════════
425
- // NEW v15: Form Interaction Tester (Playwright)
554
+ // Form Interaction Tester (Playwright)
426
555
  // ═══════════════════════════════════════════════════════════════════════════
427
556
  async function testForms(page, url) {
428
557
  const results = [];
@@ -436,19 +565,16 @@ async function testForms(page, url) {
436
565
  const submits = await form.$$('[type="submit"], button[type="submit"]');
437
566
  const hasSubmit = submits.length > 0;
438
567
 
439
- // Test required field validation
440
568
  let validationWorks = false;
441
569
  if (hasSubmit) {
442
570
  try {
443
571
  await submits[0].click({ timeout: 2000 });
444
572
  await page.waitForTimeout(300);
445
- // Check for validation messages
446
573
  const invalidFields = await page.$$(':invalid');
447
574
  validationWorks = invalidFields.length > 0 || (await page.evaluate(() => document.querySelector('.error, .invalid, [aria-invalid="true"]') !== null));
448
575
  } catch {}
449
576
  }
450
577
 
451
- // Test placeholder/label
452
578
  let labelCount = 0;
453
579
  for (const inp of inputs) {
454
580
  const id = await inp.getAttribute('id');
@@ -480,12 +606,11 @@ async function testForms(page, url) {
480
606
  }
481
607
 
482
608
  // ═══════════════════════════════════════════════════════════════════════════
483
- // NEW v15: Memory Leak Detector (Playwright)
609
+ // Memory Leak Detector (Playwright)
484
610
  // ═══════════════════════════════════════════════════════════════════════════
485
611
  async function detectMemoryLeaks(page, url) {
486
612
  const snapshots = [];
487
613
  try {
488
- // Snapshot 1: initial load
489
614
  const heap1 = await page.evaluate(() => {
490
615
  if (window.performance?.memory) {
491
616
  return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize };
@@ -494,7 +619,6 @@ async function detectMemoryLeaks(page, url) {
494
619
  });
495
620
  if (heap1) snapshots.push({ label: 'initial', ...heap1, time: 0 });
496
621
 
497
- // Simulate user interactions to trigger potential leaks
498
622
  await page.evaluate(() => {
499
623
  for (let i = 0; i < 5; i++) {
500
624
  window.dispatchEvent(new Event('scroll'));
@@ -503,7 +627,6 @@ async function detectMemoryLeaks(page, url) {
503
627
  });
504
628
  await page.waitForTimeout(1000);
505
629
 
506
- // Navigate away and back
507
630
  const currentUrl = page.url();
508
631
  await page.goto('about:blank', { waitUntil: 'load' }).catch(() => {});
509
632
  await page.goto(currentUrl, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
@@ -532,19 +655,17 @@ async function detectMemoryLeaks(page, url) {
532
655
  }
533
656
 
534
657
  // ═══════════════════════════════════════════════════════════════════════════
535
- // NEW v15: Dark Mode Tester (Playwright)
658
+ // Dark Mode Tester (Playwright)
536
659
  // ═══════════════════════════════════════════════════════════════════════════
537
660
  async function testDarkMode(page, url, screenshotDir, sessionId) {
538
661
  const results = {};
539
662
  try {
540
- // Light mode screenshot already taken — test dark mode
541
663
  await page.emulateMedia({ colorScheme: 'dark' });
542
664
  await page.waitForTimeout(800);
543
665
  const darkName = `${sessionId}-dark-${shortId()}.png`;
544
666
  const darkPath = path.join(screenshotDir, darkName);
545
667
  await page.screenshot({ path: darkPath, fullPage: false });
546
668
 
547
- // Check if dark mode actually changes anything
548
669
  const hasMediaQuery = await page.evaluate(() => {
549
670
  const sheets = [...document.styleSheets];
550
671
  for (const sheet of sheets) {
@@ -558,7 +679,6 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
558
679
  return false;
559
680
  });
560
681
 
561
- // Check body background color changes
562
682
  const darkBg = await page.evaluate(() => {
563
683
  return window.getComputedStyle(document.body).backgroundColor;
564
684
  });
@@ -567,7 +687,6 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
567
687
  results.hasMediaQuery = hasMediaQuery;
568
688
  results.supportsDark = hasMediaQuery;
569
689
 
570
- // Reset to light
571
690
  await page.emulateMedia({ colorScheme: 'light' });
572
691
  await page.waitForTimeout(300);
573
692
 
@@ -584,11 +703,10 @@ async function testDarkMode(page, url, screenshotDir, sessionId) {
584
703
  }
585
704
 
586
705
  // ═══════════════════════════════════════════════════════════════════════════
587
- // NEW v15: Third-Party Script Auditor (Playwright)
706
+ // Third-Party Script Auditor (Playwright)
588
707
  // ═══════════════════════════════════════════════════════════════════════════
589
708
  async function auditThirdPartyScripts(page) {
590
709
  const origin = new URL(page.url()).origin;
591
- const scripts = [];
592
710
  const requests = [];
593
711
 
594
712
  const handler = (req) => {
@@ -609,7 +727,6 @@ async function auditThirdPartyScripts(page) {
609
727
  await page.waitForTimeout(2000);
610
728
  page.off('request', handler);
611
729
 
612
- // Deduplicate by domain
613
730
  const domainMap = {};
614
731
  for (const r of requests) {
615
732
  if (!domainMap[r.domain]) domainMap[r.domain] = { ...r, count: 0 };
@@ -642,7 +759,7 @@ function classifyThirdParty(hostname) {
642
759
  }
643
760
 
644
761
  // ═══════════════════════════════════════════════════════════════════════════
645
- // NEW v15: Font & Asset Auditor (Playwright)
762
+ // Font & Asset Auditor (Playwright)
646
763
  // ═══════════════════════════════════════════════════════════════════════════
647
764
  async function auditFontsAndAssets(page) {
648
765
  return await page.evaluate(() => {
@@ -673,12 +790,10 @@ async function auditFontsAndAssets(page) {
673
790
  }
674
791
  }
675
792
 
676
- // Font analysis
677
793
  const fontFaces = document.fonts ? [...document.fonts].map(f => ({
678
794
  family: f.family, style: f.style, weight: f.weight, status: f.status,
679
795
  })) : [];
680
796
 
681
- // Image format analysis
682
797
  const images = [...document.images].map(img => ({
683
798
  src: img.src?.split('/').pop().slice(0, 60),
684
799
  naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight,
@@ -700,7 +815,7 @@ async function auditFontsAndAssets(page) {
700
815
  }
701
816
 
702
817
  // ═══════════════════════════════════════════════════════════════════════════
703
- // NEW v15: User Flow Simulator (Playwright)
818
+ // User Flow Simulator (Playwright)
704
819
  // ═══════════════════════════════════════════════════════════════════════════
705
820
  async function simulateUserFlow(page, url) {
706
821
  const steps = [];
@@ -770,7 +885,7 @@ async function simulateUserFlow(page, url) {
770
885
  }
771
886
 
772
887
  // ═══════════════════════════════════════════════════════════════════════════
773
- // NEW v15: Multi-Viewport Screenshot + Layout Tester (Playwright)
888
+ // Multi-Viewport Screenshot + Layout Tester (Playwright)
774
889
  // ═══════════════════════════════════════════════════════════════════════════
775
890
  async function testAllViewports(page, url, screenshotDir, sessionId) {
776
891
  const results = {};
@@ -783,11 +898,9 @@ async function testAllViewports(page, url, screenshotDir, sessionId) {
783
898
  const fpath = path.join(screenshotDir, name);
784
899
  await page.screenshot({ path: fpath, fullPage: false });
785
900
 
786
- // Check for overflow/horizontal scroll
787
901
  const hasHorizontalScroll = await page.evaluate(() =>
788
902
  document.documentElement.scrollWidth > document.documentElement.clientWidth
789
903
  );
790
- // Check font size not too small
791
904
  const minFontSize = await page.evaluate(() => {
792
905
  const els = [...document.querySelectorAll('p, span, a, li, td')].slice(0, 20);
793
906
  return Math.min(...els.map(el => parseFloat(window.getComputedStyle(el).fontSize) || 16));
@@ -807,13 +920,12 @@ async function testAllViewports(page, url, screenshotDir, sessionId) {
807
920
  results[key] = { label: vp.label, width: vp.width, height: vp.height, error: err.message, passed: false };
808
921
  }
809
922
  }
810
- // Reset to desktop
811
923
  await page.setViewportSize({ width: 1280, height: 900 });
812
924
  return results;
813
925
  }
814
926
 
815
927
  // ═══════════════════════════════════════════════════════════════════════════
816
- // NEW v15: Cache Headers Auditor
928
+ // Cache Headers Auditor
817
929
  // ═══════════════════════════════════════════════════════════════════════════
818
930
  async function auditCacheHeaders(url) {
819
931
  const r = await httpProbe(url);
@@ -848,7 +960,7 @@ async function auditCacheHeaders(url) {
848
960
  }
849
961
 
850
962
  // ═══════════════════════════════════════════════════════════════════════════
851
- // NEW v15: Mixed Content & CSP Violation Checker (Playwright)
963
+ // Mixed Content & CSP Violation Checker (Playwright)
852
964
  // ═══════════════════════════════════════════════════════════════════════════
853
965
  async function checkMixedContent(page) {
854
966
  const mixed = [];
@@ -867,7 +979,7 @@ async function checkMixedContent(page) {
867
979
  }
868
980
 
869
981
  // ═══════════════════════════════════════════════════════════════════════════
870
- // NEW v15: Error Page Tester (404, 500)
982
+ // Error Page Tester (404, 500)
871
983
  // ═══════════════════════════════════════════════════════════════════════════
872
984
  async function testErrorPages(baseUrl) {
873
985
  const tests = [
@@ -898,7 +1010,7 @@ async function testErrorPages(baseUrl) {
898
1010
  }
899
1011
 
900
1012
  // ═══════════════════════════════════════════════════════════════════════════
901
- // NEW v15: HTTP Version & TLS Inspector
1013
+ // HTTP Version & TLS Inspector
902
1014
  // ═══════════════════════════════════════════════════════════════════════════
903
1015
  async function inspectHTTPVersion(url) {
904
1016
  const r = await httpProbe(url);
@@ -910,7 +1022,7 @@ async function inspectHTTPVersion(url) {
910
1022
  return {
911
1023
  url, isHTTPS,
912
1024
  altSvc: altSvc || null,
913
- likelyHTTP2: hasH2 || isHTTPS, // Most modern HTTPS servers use H2
1025
+ likelyHTTP2: hasH2 || isHTTPS,
914
1026
  likelyHTTP3: hasH3,
915
1027
  hsts: r.headers['strict-transport-security'] || null,
916
1028
  issues: [
@@ -920,7 +1032,7 @@ async function inspectHTTPVersion(url) {
920
1032
  }
921
1033
 
922
1034
  // ═══════════════════════════════════════════════════════════════════════════
923
- // PLAYWRIGHT REAL BROWSER ENGINE v15 — Enhanced
1035
+ // PLAYWRIGHT REAL BROWSER ENGINE v15
924
1036
  // ═══════════════════════════════════════════════════════════════════════════
925
1037
  async function runPlaywrightScan(url, session, dash, options = {}) {
926
1038
  const chromium = await getPlaywright();
@@ -966,7 +1078,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
966
1078
 
967
1079
  page = await context.newPage();
968
1080
 
969
- // ── Mixed Content & CSP violations ──────────────────────────────────
970
1081
  const mixedContent = [];
971
1082
  const cspViolations2 = [];
972
1083
  page.on('console', (msg) => {
@@ -981,14 +1092,12 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
981
1092
  }
982
1093
  });
983
1094
 
984
- // ── Capture JS errors ────────────────────────────────────────────────
985
1095
  page.on('pageerror', (err) => {
986
1096
  const entry = { message: err.message, stack: err.stack, url: page.url(), timestamp: Date.now() };
987
1097
  results.jsErrors.push(entry);
988
1098
  session.consoleErrors.push({ type: 'pageerror', text: err.message, url: page.url() });
989
1099
  });
990
1100
 
991
- // ── Network monitoring ───────────────────────────────────────────────
992
1101
  const requestTimings = new Map();
993
1102
  page.on('request', (req) => {
994
1103
  requestTimings.set(req.url(), Date.now());
@@ -1015,7 +1124,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1015
1124
  }
1016
1125
  });
1017
1126
 
1018
- // ── Navigate ─────────────────────────────────────────────────────────
1019
1127
  const navStart = Date.now();
1020
1128
  const response = await page.goto(url, {
1021
1129
  waitUntil: 'networkidle', timeout: 30000,
@@ -1029,7 +1137,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1029
1137
 
1030
1138
  await fs.ensureDir(SCREENSHOT_DIR);
1031
1139
 
1032
- // ── 1. Desktop Screenshot ────────────────────────────────────────────
1140
+ // 1. Desktop Screenshot
1033
1141
  const desktopName = `${session.id}-desktop-${shortId()}.png`;
1034
1142
  const desktopPath = path.join(SCREENSHOT_DIR, desktopName);
1035
1143
  await page.screenshot({ path: desktopPath, fullPage: true });
@@ -1037,7 +1145,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1037
1145
  session.screenshots.push({ path: desktopPath, name: desktopName, type: 'desktop', url });
1038
1146
  dash?.log(chalk.green(` 📸 Desktop screenshot: ${desktopName}`));
1039
1147
 
1040
- // ── 2. Multi-Viewport Testing (v15) ──────────────────────────────────
1148
+ // 2. Multi-Viewport Testing
1041
1149
  dash?.log(chalk.cyan(' 📱 Testing all viewports...'));
1042
1150
  const vpResults = await testAllViewports(page, url, SCREENSHOT_DIR, session.id);
1043
1151
  results.viewportResults = vpResults;
@@ -1050,7 +1158,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1050
1158
  const vpIssues = Object.values(vpResults).filter(v => !v.passed);
1051
1159
  dash?.log(chalk.green(` ✓ Viewports: ${Object.keys(vpResults).length - vpIssues.length}/${Object.keys(vpResults).length} passed`));
1052
1160
 
1053
- // ── 3. Dark Mode Test (v15) ───────────────────────────────────────────
1161
+ // 3. Dark Mode Test
1054
1162
  dash?.log(chalk.cyan(' 🌙 Testing dark mode...'));
1055
1163
  const darkResult = await testDarkMode(page, url, SCREENSHOT_DIR, session.id);
1056
1164
  results.darkMode = darkResult;
@@ -1059,9 +1167,8 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1059
1167
  session.screenshots.push({ path: darkResult.dark.screenshotPath, name: darkResult.dark.screenshotName, type: 'dark-mode', url });
1060
1168
  }
1061
1169
 
1062
- // ── 4. Real Web Vitals ────────────────────────────────────────────────
1170
+ // 4. Real Web Vitals
1063
1171
  dash?.log(chalk.cyan(' ⚡ Measuring real Web Vitals...'));
1064
- // Navigate fresh for clean vitals
1065
1172
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
1066
1173
  const vitals = await page.evaluate(() => {
1067
1174
  return new Promise((resolve) => {
@@ -1102,7 +1209,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1102
1209
  results.vitals = { ...vitals, ...navTiming, navDuration };
1103
1210
  dash?.log(chalk.green(` ✓ Vitals: TTFB=${navTiming.ttfb||'?'}ms LCP=${vitals.lcp||'?'}ms FCP=${vitals.fcp||'?'}ms CLS=${vitals.cls??'?'}`));
1104
1211
 
1105
- // ── 5. Memory Leak Detection (v15) ────────────────────────────────────
1212
+ // 5. Memory Leak Detection
1106
1213
  dash?.log(chalk.cyan(' 🧠 Detecting memory leaks...'));
1107
1214
  const memResult = await detectMemoryLeaks(page, url);
1108
1215
  results.memoryLeak = memResult;
@@ -1111,7 +1218,7 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1111
1218
  dash?.log(chalk.yellow(` ⚠ Memory leak detected: +${memResult.growthMB}MB`));
1112
1219
  }
1113
1220
 
1114
- // ── 6. DOM Checks ────────────────────────────────────────────────────
1221
+ // 6. DOM Checks
1115
1222
  dash?.log(chalk.cyan(' 🔍 Running DOM checks...'));
1116
1223
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => {});
1117
1224
  const domChecks = await page.evaluate(() => {
@@ -1136,7 +1243,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1136
1243
  checks.push({ name: 'Viewport meta', pass: !!vp, value: vp?.content || 'missing' });
1137
1244
  const bodyStyle = window.getComputedStyle(document.body);
1138
1245
  checks.push({ name: 'Body has styles', pass: !!bodyStyle.backgroundColor || !!bodyStyle.color, value: 'CSS applied' });
1139
- // NEW v15 DOM checks
1140
1246
  const skipLink = document.querySelector('a[href="#main"], a[href="#content"], .skip-link');
1141
1247
  checks.push({ name: 'Skip navigation link', pass: !!skipLink, value: skipLink ? 'Present' : 'Missing (accessibility)' });
1142
1248
  const mainEl = document.querySelector('main, [role="main"]');
@@ -1161,25 +1267,25 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1161
1267
  results.domChecks = domChecks;
1162
1268
  dash?.log(chalk.green(` ✓ DOM: ${domChecks.filter(c => c.pass).length}/${domChecks.length} checks passed`));
1163
1269
 
1164
- // ── 7. Form Tests (v15) ───────────────────────────────────────────────
1270
+ // 7. Form Tests
1165
1271
  dash?.log(chalk.cyan(' 📝 Testing forms...'));
1166
1272
  const formResults = await testForms(page, url);
1167
1273
  results.forms = formResults;
1168
1274
  session.formTests.push(...formResults.map(f => ({ url, ...f })));
1169
1275
 
1170
- // ── 8. Third-Party Script Audit (v15) ─────────────────────────────────
1276
+ // 8. Third-Party Script Audit
1171
1277
  dash?.log(chalk.cyan(' 📦 Auditing third-party scripts...'));
1172
1278
  const thirdPartyScripts = await auditThirdPartyScripts(page);
1173
1279
  results.thirdParty = thirdPartyScripts;
1174
1280
  session.thirdPartyScripts.push(...thirdPartyScripts.map(s => ({ url, ...s })));
1175
1281
 
1176
- // ── 9. Font & Asset Audit (v15) ───────────────────────────────────────
1282
+ // 9. Font & Asset Audit
1177
1283
  dash?.log(chalk.cyan(' 🔤 Auditing fonts and assets...'));
1178
1284
  const assetData = await auditFontsAndAssets(page);
1179
1285
  results.fonts = assetData;
1180
1286
  session.assetAudit.push({ url, ...assetData });
1181
1287
 
1182
- // ── 10. Interaction Tests ─────────────────────────────────────────────
1288
+ // 10. Interaction Tests
1183
1289
  dash?.log(chalk.cyan(' 🖱️ Testing interactions...'));
1184
1290
  const interactions = [];
1185
1291
  const buttonCount = await page.locator('button:visible').count().catch(() => 0);
@@ -1206,14 +1312,12 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1206
1312
  const firstLink = page.locator('a:visible').first();
1207
1313
  if (await firstLink.count() > 0) { await firstLink.hover(); interactions.push({ name: 'Link hover', pass: true, value: 'Hover works' }); }
1208
1314
  } catch { interactions.push({ name: 'Link hover', pass: false, value: 'Hover failed' }); }
1209
- // NEW v15: right-click test
1210
1315
  try {
1211
1316
  await page.mouse.click(640, 400, { button: 'right' });
1212
1317
  await page.waitForTimeout(200);
1213
1318
  await page.keyboard.press('Escape');
1214
1319
  interactions.push({ name: 'Right-click (context menu)', pass: true, value: 'Works' });
1215
1320
  } catch { interactions.push({ name: 'Right-click', pass: false, value: 'Failed' }); }
1216
- // NEW v15: copy text test
1217
1321
  try {
1218
1322
  await page.keyboard.press('Control+a');
1219
1323
  await page.waitForTimeout(100);
@@ -1223,14 +1327,14 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1223
1327
  results.interactions = interactions;
1224
1328
  dash?.log(chalk.green(` ✓ Interactions: ${interactions.filter(i => i.pass).length}/${interactions.length} passed`));
1225
1329
 
1226
- // ── 11. User Flow Simulation (v15) ────────────────────────────────────
1330
+ // 11. User Flow Simulation
1227
1331
  dash?.log(chalk.cyan(' 🧑‍💻 Simulating user flow...'));
1228
1332
  const flowResult = await simulateUserFlow(page, url);
1229
1333
  results.userFlow = flowResult;
1230
1334
  session.userFlowResults.push(flowResult);
1231
1335
  dash?.log(chalk.green(` ✓ User flow: ${flowResult.passed}/${flowResult.steps.length} steps passed`));
1232
1336
 
1233
- // ── 12. Resource Analysis ─────────────────────────────────────────────
1337
+ // 12. Resource Analysis
1234
1338
  const resourceStats = await page.evaluate(() => {
1235
1339
  const entries = performance.getEntriesByType('resource');
1236
1340
  const byType = {};
@@ -1251,7 +1355,6 @@ async function runPlaywrightScan(url, session, dash, options = {}) {
1251
1355
  results.mixedContent = mixedContent;
1252
1356
  results.cspViolations = cspViolations2;
1253
1357
 
1254
- // Store mixed content/CSP in session
1255
1358
  session.mixedContentIssues.push(...mixedContent.map(m => ({ url, text: m })));
1256
1359
  session.cspViolations.push(...cspViolations2.map(c => ({ url, text: c })));
1257
1360
 
@@ -1330,7 +1433,7 @@ async function crawlSite(baseUrl, { maxPages = 60, onRoute } = {}) {
1330
1433
  }
1331
1434
  }
1332
1435
 
1333
- // Common paths probe (v15 extended)
1436
+ // Common paths probe
1334
1437
  const commonPaths = [
1335
1438
  '/api/health', '/health', '/api/status', '/api/v1/health',
1336
1439
  '/api/docs', '/robots.txt', '/sitemap.xml', '/manifest.json',
@@ -1358,7 +1461,7 @@ async function crawlSite(baseUrl, { maxPages = 60, onRoute } = {}) {
1358
1461
  }
1359
1462
 
1360
1463
  // ═══════════════════════════════════════════════════════════════════════════
1361
- // Security Scanner v15 — Extended
1464
+ // Security Scanner v15
1362
1465
  // ═══════════════════════════════════════════════════════════════════════════
1363
1466
  async function runSecurityScan(url) {
1364
1467
  const findings = [];
@@ -1388,7 +1491,6 @@ async function runSecurityScan(url) {
1388
1491
  validate: v => !v || (!v.includes('/') && !/\d+\.\d+/.test(v)), rec: 'Genericize Server header' },
1389
1492
  { id: 'xpb', name: 'X-Powered-By hidden', header: 'x-powered-by', sev: 'P2',
1390
1493
  validate: v => !v, rec: 'Remove X-Powered-By header' },
1391
- // NEW v15
1392
1494
  { id: 'coep', name: 'Cross-Origin-Embedder-Policy', header: 'cross-origin-embedder-policy', sev: 'P3',
1393
1495
  validate: v => !!v, rec: 'Add COEP for isolation' },
1394
1496
  { id: 'coop', name: 'Cross-Origin-Opener-Policy', header: 'cross-origin-opener-policy', sev: 'P3',
@@ -1421,7 +1523,6 @@ async function runSecurityScan(url) {
1421
1523
  recommendation: 'Never combine CORS * with allow-credentials',
1422
1524
  });
1423
1525
 
1424
- // NEW v15: Check for version disclosure in other headers
1425
1526
  const versionHeaders = ['x-aspnet-version', 'x-aspnetmvc-version', 'x-drupal-cache', 'x-generator'];
1426
1527
  for (const vh of versionHeaders) {
1427
1528
  if (h[vh]) findings.push({
@@ -1444,7 +1545,6 @@ async function runSecurityScan(url) {
1444
1545
  { path: '/api/openapi.json', name: 'OpenAPI docs exposed' },
1445
1546
  { path: '/config.json', name: 'config.json exposed' },
1446
1547
  { path: '/debug', name: 'Debug endpoint' },
1447
- // NEW v15
1448
1548
  { path: '/.DS_Store', name: '.DS_Store exposed' },
1449
1549
  { path: '/wp-config.php', name: 'WordPress config' },
1450
1550
  { path: '/package.json', name: 'package.json exposed' },
@@ -1474,7 +1574,7 @@ async function runSecurityScan(url) {
1474
1574
  }
1475
1575
 
1476
1576
  // ═══════════════════════════════════════════════════════════════════════════
1477
- // SEO Scanner v15 — Extended
1577
+ // SEO Scanner v15
1478
1578
  // ═══════════════════════════════════════════════════════════════════════════
1479
1579
  async function runSEOScan(url) {
1480
1580
  const t0 = Date.now();
@@ -1535,7 +1635,6 @@ async function runSEOScan(url) {
1535
1635
  checks.push({ name: 'Server response time', pass: rt < 800, severity: rt > 2000 ? 'P1' : 'P2',
1536
1636
  category: 'performance-seo', detail: `TTFB: ${rt}ms (Google: <800ms)` });
1537
1637
 
1538
- // NEW v15: Heading hierarchy
1539
1638
  const headings = (html.match(/<h[1-6][^>]*>/gi) || []).map(h => parseInt(h[2]));
1540
1639
  let hierOk = true;
1541
1640
  for (let i = 1; i < headings.length; i++) {
@@ -1543,7 +1642,6 @@ async function runSEOScan(url) {
1543
1642
  }
1544
1643
  checks.push({ name: 'Heading hierarchy', pass: hierOk, severity: 'P2', category: 'structure', detail: hierOk ? 'Headings in order' : 'Skipped heading levels' });
1545
1644
 
1546
- // NEW v15: noindex check
1547
1645
  const noindex = has(/<meta[^>]+name=["']robots["'][^>]+content=["'][^"']*noindex/i);
1548
1646
  checks.push({ name: 'Not noindexed', pass: !noindex, severity: noindex ? 'P1' : 'INFO', category: 'crawling', detail: noindex ? 'Page is noindexed!' : 'Indexable' });
1549
1647
 
@@ -1562,7 +1660,7 @@ async function runSEOScan(url) {
1562
1660
  }
1563
1661
 
1564
1662
  // ═══════════════════════════════════════════════════════════════════════════
1565
- // Accessibility Scanner v15 — Extended
1663
+ // Accessibility Scanner v15
1566
1664
  // ═══════════════════════════════════════════════════════════════════════════
1567
1665
  async function runA11yScan(url) {
1568
1666
  const r = await httpProbe(url, { timeout: 12000 });
@@ -1578,7 +1676,6 @@ async function runA11yScan(url) {
1578
1676
  { id: 'h1-present', impact: 'moderate', test: () => !/<h1[^>]*>/i.test(html), pass: 'H1 heading present', desc: 'Page should have H1' },
1579
1677
  { id: 'link-text', impact: 'serious', test: () => /<a[^>]*>\s*<\/a>/i.test(html), pass: 'Links have text', desc: 'Links must have discernible text' },
1580
1678
  { 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
1679
  { 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
1680
  { 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
1681
  { 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 +1700,7 @@ async function runA11yScan(url) {
1603
1700
  }
1604
1701
 
1605
1702
  // ═══════════════════════════════════════════════════════════════════════════
1606
- // AI Bug Classifier v15 — Enhanced patterns
1703
+ // AI Bug Classifier v15
1607
1704
  // ═══════════════════════════════════════════════════════════════════════════
1608
1705
  const SEV_PATTERNS = {
1609
1706
  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 +1744,7 @@ function classifyBug(bug) {
1647
1744
  }
1648
1745
 
1649
1746
  // ═══════════════════════════════════════════════════════════════════════════
1650
- // Terminal Dashboard v15 — Enhanced live display
1747
+ // Terminal Dashboard v15
1651
1748
  // ═══════════════════════════════════════════════════════════════════════════
1652
1749
  class TerminalDashboard {
1653
1750
  #session; #lines = 0; #active = false; #timer = null;
@@ -1777,7 +1874,7 @@ class TerminalDashboard {
1777
1874
  }
1778
1875
 
1779
1876
  // ═══════════════════════════════════════════════════════════════════════════
1780
- // HTML Report Builder v15 — Ultra Rich
1877
+ // HTML Report Builder v15
1781
1878
  // ═══════════════════════════════════════════════════════════════════════════
1782
1879
  function buildHTMLReport(session) {
1783
1880
  const summary = session.getSummary();
@@ -1799,14 +1896,6 @@ function buildHTMLReport(session) {
1799
1896
 
1800
1897
  const esc = (s) => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1801
1898
 
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
1899
  const screenshotCards = session.screenshots.length
1811
1900
  ? session.screenshots.map(sc => {
1812
1901
  let imgTag = '';
@@ -1831,7 +1920,6 @@ function buildHTMLReport(session) {
1831
1920
  }).join('')
1832
1921
  : '<p class="no-data">No screenshots (Playwright not available)</p>';
1833
1922
 
1834
- // ── Test rows ─────────────────────────────────────────────────────────────
1835
1923
  const testRows = session.results.map(r => `
1836
1924
  <tr class="result-row" data-type="${r.type}" data-status="${r.status}">
1837
1925
  <td>${esc(r.name)}</td>
@@ -1842,7 +1930,6 @@ function buildHTMLReport(session) {
1842
1930
  <td class="err-cell">${r.message ? `<details><summary>Details</summary><pre>${esc(String(r.message).slice(0,500))}</pre></details>` : '✓'}</td>
1843
1931
  </tr>`).join('');
1844
1932
 
1845
- // ── Bug cards ─────────────────────────────────────────────────────────────
1846
1933
  const bugCards = session.bugs.length
1847
1934
  ? session.bugs.map(b => `
1848
1935
  <div class="bug-card sev-border-${(b.aiSeverity||b.severity||'p3').toLowerCase()}" data-severity="${b.aiSeverity||b.severity}">
@@ -1859,7 +1946,6 @@ function buildHTMLReport(session) {
1859
1946
  </div>`).join('')
1860
1947
  : '<p class="no-data">No bugs detected 🎉</p>';
1861
1948
 
1862
- // ── Route rows ────────────────────────────────────────────────────────────
1863
1949
  const routeRows = session.routeMap.map(r => `
1864
1950
  <tr>
1865
1951
  <td><code class="url">${esc(r.url)}</code></td>
@@ -1870,7 +1956,6 @@ function buildHTMLReport(session) {
1870
1956
  <td>${r.error ? `<span class="fail">${esc(r.error)}</span>` : '✓'}</td>
1871
1957
  </tr>`).join('');
1872
1958
 
1873
- // ── Security rows ─────────────────────────────────────────────────────────
1874
1959
  const secRows = session.secFindings.map(f => `
1875
1960
  <tr class="${f.pass ? '' : 'fail-row'}">
1876
1961
  <td>${esc(f.check)}</td>
@@ -1881,7 +1966,6 @@ function buildHTMLReport(session) {
1881
1966
  <td>${f.recommendation ? `<span class="rec">${esc(f.recommendation)}</span>` : '–'}</td>
1882
1967
  </tr>`).join('');
1883
1968
 
1884
- // ── SEO section ───────────────────────────────────────────────────────────
1885
1969
  const seoSection = session.seoResults.map(r => `
1886
1970
  <div class="seo-page">
1887
1971
  <div class="seo-header">
@@ -1898,7 +1982,6 @@ function buildHTMLReport(session) {
1898
1982
  </table>
1899
1983
  </div>`).join('') || '<p class="no-data">No SEO scans</p>';
1900
1984
 
1901
- // ── A11y section ──────────────────────────────────────────────────────────
1902
1985
  const a11ySection = session.a11yResults.map(r => `
1903
1986
  <div class="a11y-page">
1904
1987
  <div class="a11y-header">
@@ -1912,7 +1995,6 @@ function buildHTMLReport(session) {
1912
1995
  </div>`).join('') || '<p class="no-data">No violations ✓</p>'}
1913
1996
  </div>`).join('') || '<p class="no-data">No accessibility scans</p>';
1914
1997
 
1915
- // ── Performance section ───────────────────────────────────────────────────
1916
1998
  const vitalCard = (name, value, threshold, unit) => {
1917
1999
  const na = value === null || value === undefined;
1918
2000
  const pass2 = !na && value <= threshold;
@@ -1965,7 +2047,6 @@ function buildHTMLReport(session) {
1965
2047
  </div>`;
1966
2048
  }).join('') || '<p class="no-data">No performance data</p>';
1967
2049
 
1968
- // ── NEW v15: Load Test section ────────────────────────────────────────────
1969
2050
  const loadTestSection = session.loadTestResults.length
1970
2051
  ? session.loadTestResults.map(lt => `
1971
2052
  <div class="load-test-card ${lt.passed ? 'lt-pass' : 'lt-fail'}">
@@ -1980,9 +2061,8 @@ function buildHTMLReport(session) {
1980
2061
  <p style="color:#94a3b8;font-size:.8rem;margin-top:.75rem">${lt.requests} requests · ${lt.errors} errors · ${lt.timeouts} timeouts · ${formatDuration(lt.duration)}</p>
1981
2062
  <p style="color:#94a3b8;font-size:.78rem">Status codes: ${Object.entries(lt.responses||{}).map(([k,v]) => `${k}: ${v}`).join(', ')}</p>
1982
2063
  </div>`).join('')
1983
- : '<p class="no-data">Load test not run (use runUrlQA with loadTest:true)</p>';
2064
+ : '<p class="no-data">Load test not run</p>';
1984
2065
 
1985
- // ── NEW v15: Viewport section ─────────────────────────────────────────────
1986
2066
  const vpSection = Object.keys(session.viewportResults || {}).length
1987
2067
  ? `<div class="viewport-grid">${Object.entries(session.viewportResults).map(([key, vp]) => `
1988
2068
  <div class="vp-card ${vp.passed ? '' : 'vp-fail'}">
@@ -1993,7 +2073,6 @@ function buildHTMLReport(session) {
1993
2073
  </div>`).join('')}</div>`
1994
2074
  : '<p class="no-data">No viewport tests (Playwright required)</p>';
1995
2075
 
1996
- // ── NEW v15: Broken links section ─────────────────────────────────────────
1997
2076
  const brokenLinksSection = session.brokenLinks.length
1998
2077
  ? session.brokenLinks.map(bl => `
1999
2078
  <div class="card" style="margin-bottom:1rem">
@@ -2009,7 +2088,6 @@ function buildHTMLReport(session) {
2009
2088
  </div>`).join('')
2010
2089
  : '<p class="no-data">No broken link scans run</p>';
2011
2090
 
2012
- // ── NEW v15: Cookie audit ─────────────────────────────────────────────────
2013
2091
  const cookieSection = session.cookieAudit.length
2014
2092
  ? session.cookieAudit.map(ca => `
2015
2093
  <div class="card" style="margin-bottom:1rem">
@@ -2027,7 +2105,6 @@ function buildHTMLReport(session) {
2027
2105
  </div>`).join('')
2028
2106
  : '<p class="no-data">No cookie audits</p>';
2029
2107
 
2030
- // ── NEW v15: Third-party scripts ──────────────────────────────────────────
2031
2108
  const thirdPartySection = session.thirdPartyScripts.length
2032
2109
  ? `<table>
2033
2110
  <thead><tr><th>Vendor</th><th>Domain</th><th>Count</th><th>URL</th></tr></thead>
@@ -2040,7 +2117,6 @@ function buildHTMLReport(session) {
2040
2117
  </table>`
2041
2118
  : '<p class="no-data">No third-party scripts detected</p>';
2042
2119
 
2043
- // ── NEW v15: User Flow section ────────────────────────────────────────────
2044
2120
  const userFlowSection = session.userFlowResults.length
2045
2121
  ? session.userFlowResults.map(f => `
2046
2122
  <div class="card" style="margin-bottom:1rem">
@@ -2057,7 +2133,6 @@ function buildHTMLReport(session) {
2057
2133
  </div>`).join('')
2058
2134
  : '<p class="no-data">No user flow simulations</p>';
2059
2135
 
2060
- // ── NEW v15: Memory section ───────────────────────────────────────────────
2061
2136
  const memorySection = session.memorySnapshots.length
2062
2137
  ? session.memorySnapshots.map(m => `
2063
2138
  <div class="card" style="margin-bottom:1rem">
@@ -2068,11 +2143,10 @@ function buildHTMLReport(session) {
2068
2143
  ${m.snapshots[0] ? vitalCard('Init Heap', Math.round(m.snapshots[0].used/1024/1024), 50, 'MB') : ''}
2069
2144
  ${m.snapshots[1] ? vitalCard('After Nav', Math.round(m.snapshots[1].used/1024/1024), 60, 'MB') : ''}
2070
2145
  </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>'}
2146
+ ${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
2147
  </div>`).join('')
2073
2148
  : '<p class="no-data">No memory tests (Playwright required)</p>';
2074
2149
 
2075
- // ── NEW v15: Dark mode section ────────────────────────────────────────────
2076
2150
  const darkModeSection = session.darkModeResults.length
2077
2151
  ? session.darkModeResults.map(dm => `
2078
2152
  <div class="card" style="margin-bottom:1rem">
@@ -2082,11 +2156,10 @@ function buildHTMLReport(session) {
2082
2156
  <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
2157
  <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
2158
  </div>
2085
- ${!dm.supportsDark ? `<div class="bug-rec">💡 Consider adding dark mode with <code>@media (prefers-color-scheme: dark)</code></div>` : ''}
2159
+ ${!dm.supportsDark ? `<div class="bug-rec">💡 Add dark mode with <code>@media (prefers-color-scheme: dark)</code></div>` : ''}
2086
2160
  </div>`).join('')
2087
2161
  : '<p class="no-data">No dark mode tests (Playwright required)</p>';
2088
2162
 
2089
- // ── NEW v15: Redirect chains section ─────────────────────────────────────
2090
2163
  const redirectSection = session.redirectChains.length
2091
2164
  ? session.redirectChains.map(rc => `
2092
2165
  <div class="card" style="margin-bottom:1rem">
@@ -2104,7 +2177,6 @@ function buildHTMLReport(session) {
2104
2177
  </div>`).join('')
2105
2178
  : '<p class="no-data">No redirect chains analyzed</p>';
2106
2179
 
2107
- // ── Form tests section ────────────────────────────────────────────────────
2108
2180
  const formTestSection = session.formTests.length
2109
2181
  ? session.formTests.map(f => `
2110
2182
  <div class="card" style="margin-bottom:1rem">
@@ -2119,7 +2191,6 @@ function buildHTMLReport(session) {
2119
2191
  </div>`).join('')
2120
2192
  : '<p class="no-data">No forms found or Playwright not available</p>';
2121
2193
 
2122
- // ── Cache headers section ─────────────────────────────────────────────────
2123
2194
  const cacheSection = session.cacheHeaders.length
2124
2195
  ? `<table>
2125
2196
  <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 +2205,6 @@ function buildHTMLReport(session) {
2134
2205
  </table>`
2135
2206
  : '<p class="no-data">No cache audits</p>';
2136
2207
 
2137
- // ── Error pages section ───────────────────────────────────────────────────
2138
2208
  const errorPageSection = session.errorPageTests.length
2139
2209
  ? `<table>
2140
2210
  <thead><tr><th>Test</th><th>Actual Status</th><th>Custom Page</th><th>Status</th></tr></thead>
@@ -2147,7 +2217,6 @@ function buildHTMLReport(session) {
2147
2217
  </table>`
2148
2218
  : '<p class="no-data">No error page tests</p>';
2149
2219
 
2150
- // ── Console errors table ──────────────────────────────────────────────────
2151
2220
  const consoleSection = session.consoleErrors.length
2152
2221
  ? `<table>
2153
2222
  <thead><tr><th>Type</th><th>Message</th><th>URL</th></tr></thead>
@@ -2159,7 +2228,6 @@ function buildHTMLReport(session) {
2159
2228
  </table>`
2160
2229
  : '<p class="no-data">No console errors 🎉</p>';
2161
2230
 
2162
- // ── Network failures table ────────────────────────────────────────────────
2163
2231
  const networkSection = session.networkLog.length
2164
2232
  ? `<table>
2165
2233
  <thead><tr><th>URL</th><th>Method</th><th>Failure</th></tr></thead>
@@ -2171,7 +2239,6 @@ function buildHTMLReport(session) {
2171
2239
  </table>`
2172
2240
  : '<p class="no-data">No network failures 🎉</p>';
2173
2241
 
2174
- // Mixed content
2175
2242
  const mixedContentSection = session.mixedContentIssues.length
2176
2243
  ? `<table>
2177
2244
  <thead><tr><th>URL</th><th>Issue</th></tr></thead>
@@ -2201,7 +2268,7 @@ function buildHTMLReport(session) {
2201
2268
  <head>
2202
2269
  <meta charset="UTF-8">
2203
2270
  <meta name="viewport" content="width=device-width,initial-scale=1">
2204
- <title>Backlist QA v15 Report — ${esc(session.id)}</title>
2271
+ <title>Backlist QA v${VERSION} Report — ${esc(session.id)}</title>
2205
2272
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
2206
2273
  <style>
2207
2274
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap');
@@ -2309,7 +2376,7 @@ footer{text-align:center;color:var(--dim);font-size:.68rem;padding:2rem;border-t
2309
2376
  <body>
2310
2377
  <header>
2311
2378
  <div>
2312
- <div class="logo">⚡ Backlist Enterprise QA v15</div>
2379
+ <div class="logo">⚡ Backlist Enterprise QA v${VERSION}</div>
2313
2380
  <div class="header-meta">
2314
2381
  Run: ${esc(session.id)} · ${new Date(session.startedAt).toLocaleString()} · ${formatDuration(summary.duration)} · v${VERSION}
2315
2382
  </div>
@@ -2349,7 +2416,6 @@ footer{text-align:center;color:var(--dim);font-size:.68rem;padding:2rem;border-t
2349
2416
  ${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
2417
  <div class="real-banner">✅ <strong>100% Real Runtime Data</strong> — All results from actual HTTP requests and live Chromium browser testing.</div>
2351
2418
 
2352
- <!-- OVERVIEW -->
2353
2419
  <div id="tab-overview" class="tab-panel active">
2354
2420
  ${urlsStr ? `<div class="card"><div class="card-title">Target URLs</div>${urlsStr}</div>` : ''}
2355
2421
  <div class="metrics">
@@ -2376,7 +2442,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2376
2442
  </div>
2377
2443
  </div>
2378
2444
 
2379
- <!-- SCREENSHOTS -->
2380
2445
  <div id="tab-screenshots" class="tab-panel">
2381
2446
  <div class="card">
2382
2447
  <div class="card-title">Browser Screenshots <span>${session.screenshots.length} captured (${vpCount} viewports + dark mode)</span></div>
@@ -2384,7 +2449,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2384
2449
  </div>
2385
2450
  </div>
2386
2451
 
2387
- <!-- VIEWPORTS -->
2388
2452
  <div id="tab-viewports" class="tab-panel">
2389
2453
  <div class="card">
2390
2454
  <div class="card-title">Multi-Viewport Testing <span>${vpCount} viewports</span></div>
@@ -2392,7 +2456,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2392
2456
  </div>
2393
2457
  </div>
2394
2458
 
2395
- <!-- TESTS -->
2396
2459
  <div id="tab-tests" class="tab-panel">
2397
2460
  <div class="search-bar">
2398
2461
  <input type="text" id="testSearch" placeholder="Search tests..." onkeyup="filterTests()">
@@ -2415,7 +2478,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2415
2478
  </div>
2416
2479
  </div>
2417
2480
 
2418
- <!-- BUGS -->
2419
2481
  <div id="tab-bugs" class="tab-panel">
2420
2482
  <div class="search-bar">
2421
2483
  <input type="text" id="bugSearch" placeholder="Search bugs..." onkeyup="filterBugs()">
@@ -2432,7 +2494,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2432
2494
  <div id="bugList">${bugCards}</div>
2433
2495
  </div>
2434
2496
 
2435
- <!-- ROUTES -->
2436
2497
  <div id="tab-routes" class="tab-panel">
2437
2498
  <div class="card">
2438
2499
  <div class="card-title">Discovered Routes <span>${session.routeMap.length} pages/APIs</span></div>
@@ -2443,7 +2504,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2443
2504
  </div>
2444
2505
  </div>
2445
2506
 
2446
- <!-- SECURITY -->
2447
2507
  <div id="tab-security" class="tab-panel">
2448
2508
  <div class="card">
2449
2509
  <div class="card-title">Security Scan <span>${session.secFindings.length} checks · ${session.secFindings.filter(f=>!f.pass).length} issues</span></div>
@@ -2454,73 +2514,61 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2454
2514
  </div>
2455
2515
  </div>
2456
2516
 
2457
- <!-- PERFORMANCE -->
2458
2517
  <div id="tab-performance" class="tab-panel">
2459
2518
  <div class="card-title" style="padding:.5rem 0 1rem">Performance — Real Web Vitals (Playwright Chromium)</div>
2460
2519
  ${perfSection}
2461
2520
  </div>
2462
2521
 
2463
- <!-- LOAD TEST -->
2464
2522
  <div id="tab-loadtest" class="tab-panel">
2465
2523
  <div class="card-title" style="padding:.5rem 0 1rem">Load Testing — Concurrent Requests</div>
2466
2524
  ${loadTestSection}
2467
2525
  </div>
2468
2526
 
2469
- <!-- A11Y -->
2470
2527
  <div id="tab-a11y" class="tab-panel">
2471
2528
  <div class="card-title" style="padding:.5rem 0 1rem">Accessibility — WCAG 2.1 HTML Analysis (15 rules)</div>
2472
2529
  ${a11ySection}
2473
2530
  </div>
2474
2531
 
2475
- <!-- SEO -->
2476
2532
  <div id="tab-seo" class="tab-panel">
2477
2533
  <div class="card-title" style="padding:.5rem 0 1rem">SEO Analysis — Googlebot User-Agent (21 checks)</div>
2478
2534
  ${seoSection}
2479
2535
  </div>
2480
2536
 
2481
- <!-- DARK MODE -->
2482
2537
  <div id="tab-darkmode" class="tab-panel">
2483
2538
  <div class="card-title" style="padding:.5rem 0 1rem">Dark Mode Testing</div>
2484
2539
  ${darkModeSection}
2485
2540
  </div>
2486
2541
 
2487
- <!-- USER FLOW -->
2488
2542
  <div id="tab-userflow" class="tab-panel">
2489
2543
  <div class="card-title" style="padding:.5rem 0 1rem">User Flow Simulation</div>
2490
2544
  ${userFlowSection}
2491
2545
  </div>
2492
2546
 
2493
- <!-- FORMS -->
2494
2547
  <div id="tab-forms" class="tab-panel">
2495
2548
  <div class="card-title" style="padding:.5rem 0 1rem">Form Testing</div>
2496
2549
  ${formTestSection}
2497
2550
  </div>
2498
2551
 
2499
- <!-- COOKIES -->
2500
2552
  <div id="tab-cookies" class="tab-panel">
2501
2553
  <div class="card-title" style="padding:.5rem 0 1rem">Cookie Security Audit</div>
2502
2554
  ${cookieSection}
2503
2555
  </div>
2504
2556
 
2505
- <!-- MEMORY -->
2506
2557
  <div id="tab-memory" class="tab-panel">
2507
2558
  <div class="card-title" style="padding:.5rem 0 1rem">Memory Leak Detection</div>
2508
2559
  ${memorySection}
2509
2560
  </div>
2510
2561
 
2511
- <!-- BROKEN LINKS -->
2512
2562
  <div id="tab-brokenlinks" class="tab-panel">
2513
2563
  <div class="card-title" style="padding:.5rem 0 1rem">Broken Link Scanner</div>
2514
2564
  ${brokenLinksSection}
2515
2565
  </div>
2516
2566
 
2517
- <!-- REDIRECTS -->
2518
2567
  <div id="tab-redirects" class="tab-panel">
2519
2568
  <div class="card-title" style="padding:.5rem 0 1rem">Redirect Chain Analysis</div>
2520
2569
  ${redirectSection}
2521
2570
  </div>
2522
2571
 
2523
- <!-- THIRD PARTY -->
2524
2572
  <div id="tab-thirdparty" class="tab-panel">
2525
2573
  <div class="card">
2526
2574
  <div class="card-title">Third-Party Script Audit <span>${session.thirdPartyScripts.length} external scripts</span></div>
@@ -2528,7 +2576,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2528
2576
  </div>
2529
2577
  </div>
2530
2578
 
2531
- <!-- CACHE -->
2532
2579
  <div id="tab-cache" class="tab-panel">
2533
2580
  <div class="card">
2534
2581
  <div class="card-title">Cache Headers Audit</div>
@@ -2536,7 +2583,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2536
2583
  </div>
2537
2584
  </div>
2538
2585
 
2539
- <!-- ERROR PAGES -->
2540
2586
  <div id="tab-errorpages" class="tab-panel">
2541
2587
  <div class="card">
2542
2588
  <div class="card-title">Error Page Testing</div>
@@ -2544,7 +2590,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2544
2590
  </div>
2545
2591
  </div>
2546
2592
 
2547
- <!-- MIXED CONTENT -->
2548
2593
  <div id="tab-mixed" class="tab-panel">
2549
2594
  <div class="card">
2550
2595
  <div class="card-title">Mixed Content Issues</div>
@@ -2552,7 +2597,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2552
2597
  </div>
2553
2598
  </div>
2554
2599
 
2555
- <!-- CONSOLE -->
2556
2600
  <div id="tab-console" class="tab-panel">
2557
2601
  <div class="card">
2558
2602
  <div class="card-title">Console Errors &amp; Warnings <span>${session.consoleErrors.length} entries</span></div>
@@ -2560,7 +2604,6 @@ ${session.playwrightMode ? '<div class="pw-banner">🎭 <strong>BACKLIST Real Br
2560
2604
  </div>
2561
2605
  </div>
2562
2606
 
2563
- <!-- NETWORK -->
2564
2607
  <div id="tab-network" class="tab-panel">
2565
2608
  <div class="card">
2566
2609
  <div class="card-title">Network Failures <span>${session.networkLog.length} failures</span></div>
@@ -2626,7 +2669,7 @@ async function runQAEngine(session, opts = {}) {
2626
2669
  };
2627
2670
 
2628
2671
  try {
2629
- // ── Phase 1: Discovery ───────────────────────────────────────────────
2672
+ // Phase 1: Discovery
2630
2673
  dash.setPhase('🔍 Phase 1: Route Discovery & Crawling');
2631
2674
  for (const [label, url] of Object.entries(session.urls)) {
2632
2675
  if (!url) continue;
@@ -2644,7 +2687,7 @@ async function runQAEngine(session, opts = {}) {
2644
2687
  message: `Discovered ${routes.length} routes in ${Date.now()-t0}ms`, url, label });
2645
2688
  }
2646
2689
 
2647
- // ── Phase 2: Redirect Chain Analysis (v15) ───────────────────────────
2690
+ // Phase 2: Redirect Chain Analysis
2648
2691
  dash.setPhase('↪ Phase 2: Redirect Chain Analysis');
2649
2692
  for (const [label, url] of Object.entries(session.urls)) {
2650
2693
  if (!url) continue;
@@ -2660,7 +2703,7 @@ async function runQAEngine(session, opts = {}) {
2660
2703
  }
2661
2704
  }
2662
2705
 
2663
- // ── Phase 3: Playwright Real Browser ────────────────────────────────
2706
+ // Phase 3: Playwright Real Browser
2664
2707
  dash.setPhase('🎭 Phase 3: Playwright Real Browser Tests');
2665
2708
  const chromium = await getPlaywright();
2666
2709
 
@@ -2698,7 +2741,6 @@ async function runQAEngine(session, opts = {}) {
2698
2741
  if (!i.pass) session.addBug({ title: `Interaction Failed: ${i.name}`, severity: 'P2', type: 'javascript', url, evidence: { value: i.value } });
2699
2742
  }
2700
2743
 
2701
- // Viewport results
2702
2744
  for (const [vk, vp] of Object.entries(pw.viewportResults || {})) {
2703
2745
  if (!vp.error) {
2704
2746
  addResult({ name: `Viewport: ${vp.label}`, type: 'viewport',
@@ -2707,7 +2749,6 @@ async function runQAEngine(session, opts = {}) {
2707
2749
  }
2708
2750
  }
2709
2751
 
2710
- // User flow
2711
2752
  if (pw.userFlow?.steps) {
2712
2753
  for (const step of pw.userFlow.steps) {
2713
2754
  addResult({ name: `Flow: ${step.name}`, type: 'user-flow',
@@ -2715,14 +2756,12 @@ async function runQAEngine(session, opts = {}) {
2715
2756
  }
2716
2757
  }
2717
2758
 
2718
- // Dark mode
2719
2759
  if (pw.darkMode && !pw.darkMode.error) {
2720
2760
  addResult({ name: `[${label}] Dark Mode Support`, type: 'dark-mode',
2721
2761
  status: pw.darkMode.supportsDark ? 'PASS' : 'FAIL',
2722
2762
  message: pw.darkMode.supportsDark ? 'prefers-color-scheme supported' : 'No dark mode support', url, label });
2723
2763
  }
2724
2764
 
2725
- // Memory
2726
2765
  if (pw.memoryLeak) {
2727
2766
  addResult({ name: `[${label}] Memory Leak Check`, type: 'memory',
2728
2767
  status: pw.memoryLeak.hasLeak ? 'FAIL' : 'PASS',
@@ -2730,39 +2769,33 @@ async function runQAEngine(session, opts = {}) {
2730
2769
  if (pw.memoryLeak.hasLeak) session.addBug({ title: `Memory leak: +${pw.memoryLeak.growthMB}MB`, severity: pw.memoryLeak.severity, type: 'performance', url, evidence: pw.memoryLeak });
2731
2770
  }
2732
2771
 
2733
- // Form tests
2734
2772
  for (const f of pw.forms || []) {
2735
2773
  addResult({ name: `Form #${f.formIndex+1}: ${f.action||'self'}`, type: 'form',
2736
2774
  status: f.passed ? 'PASS' : 'FAIL', message: (f.issues||[]).join(', ') || 'OK', url, label });
2737
2775
  }
2738
2776
 
2739
- // Third-party
2740
2777
  if (pw.thirdParty?.length > 0) {
2741
2778
  addResult({ name: `[${label}] Third-party scripts`, type: 'third-party',
2742
2779
  status: 'PASS', message: `${pw.thirdParty.length} external scripts: ${pw.thirdParty.map(t=>t.vendor).join(', ')}`, url, label });
2743
2780
  }
2744
2781
 
2745
- // JS errors
2746
2782
  for (const err of pw.jsErrors || []) {
2747
2783
  addResult({ name: `JS Error: ${err.message?.slice(0,60)}`, type: 'javascript',
2748
2784
  status: 'FAIL', message: err.message, url, label, severity: 'P2' });
2749
2785
  session.addBug({ title: `JS Error: ${err.message?.slice(0,80)}`, severity: 'P2', type: 'javascript', url, evidence: { message: err.message } });
2750
2786
  }
2751
2787
 
2752
- // Network failures
2753
2788
  for (const fail of pw.networkFails || []) {
2754
2789
  addResult({ name: `Network Fail: ${fail.url?.split('/').pop()?.slice(0,40)}`, type: 'network',
2755
2790
  status: 'FAIL', message: fail.failure || `HTTP ${fail.status}`, url: fail.url, label });
2756
2791
  session.addBug({ title: `Network Failure: ${fail.url?.split('/').pop()}`, severity: fail.status >= 500 ? 'P1' : 'P2', type: 'network', url: fail.url });
2757
2792
  }
2758
2793
 
2759
- // Mixed content
2760
2794
  for (const mc of pw.mixedContent || []) {
2761
2795
  addResult({ name: `Mixed Content`, type: 'security', status: 'FAIL', message: mc, url, label });
2762
2796
  session.addBug({ title: `Mixed Content detected`, severity: 'P1', type: 'security', url, evidence: { text: mc } });
2763
2797
  }
2764
2798
 
2765
- // Web Vitals
2766
2799
  const { lcp, fcp, cls, tbt, ttfb } = pw.vitals || {};
2767
2800
  const vitalTests = [
2768
2801
  { name: 'TTFB', val: ttfb || pw.vitals?.ttfb, threshold: 800 },
@@ -2807,7 +2840,7 @@ async function runQAEngine(session, opts = {}) {
2807
2840
  }
2808
2841
  }
2809
2842
 
2810
- // ── Phase 4: API Validation ──────────────────────────────────────────
2843
+ // Phase 4: API Validation
2811
2844
  dash.setPhase('📡 Phase 4: API Validation & Contract Testing');
2812
2845
  const apiRoutes = session.routeMap.filter(r => r.type === 'api' || r.url?.includes('/api/'));
2813
2846
  dash.log(`Validating ${apiRoutes.length} API endpoints...`);
@@ -2824,7 +2857,7 @@ async function runQAEngine(session, opts = {}) {
2824
2857
  description: contract.issues.join(', '), evidence: { status: contract.status, issues: contract.issues } });
2825
2858
  }
2826
2859
 
2827
- // ── Phase 5: Security ────────────────────────────────────────────────
2860
+ // Phase 5: Security
2828
2861
  dash.setPhase('🛡️ Phase 5: Security Scan (20+ checks)');
2829
2862
  for (const [label, url] of Object.entries(session.urls)) {
2830
2863
  if (!url) continue;
@@ -2841,7 +2874,7 @@ async function runQAEngine(session, opts = {}) {
2841
2874
  }
2842
2875
  }
2843
2876
 
2844
- // ── Phase 6: Cookie Audit (v15) ──────────────────────────────────────
2877
+ // Phase 6: Cookie Audit
2845
2878
  dash.setPhase('🍪 Phase 6: Cookie Security Audit');
2846
2879
  for (const [label, url] of Object.entries(session.urls)) {
2847
2880
  if (!url) continue;
@@ -2858,7 +2891,7 @@ async function runQAEngine(session, opts = {}) {
2858
2891
  }
2859
2892
  }
2860
2893
 
2861
- // ── Phase 7: Cache Headers (v15) ─────────────────────────────────────
2894
+ // Phase 7: Cache Headers
2862
2895
  dash.setPhase('💾 Phase 7: Cache Headers Audit');
2863
2896
  for (const [label, url] of Object.entries(session.urls)) {
2864
2897
  if (!url) continue;
@@ -2869,7 +2902,7 @@ async function runQAEngine(session, opts = {}) {
2869
2902
  message: cacheResult.cacheControl || 'No cache headers', url, label });
2870
2903
  }
2871
2904
 
2872
- // ── Phase 8: Broken Links (v15) ──────────────────────────────────────
2905
+ // Phase 8: Broken Links
2873
2906
  dash.setPhase('🔗 Phase 8: Broken Link Scanner');
2874
2907
  const pageRoutes8 = session.routeMap.filter(r => r.type === 'page').slice(0, 5);
2875
2908
  for (const route of pageRoutes8) {
@@ -2885,7 +2918,7 @@ async function runQAEngine(session, opts = {}) {
2885
2918
  }
2886
2919
  }
2887
2920
 
2888
- // ── Phase 9: Error Pages (v15) ───────────────────────────────────────
2921
+ // Phase 9: Error Pages
2889
2922
  dash.setPhase('🚫 Phase 9: Error Page Testing');
2890
2923
  for (const [label, url] of Object.entries(session.urls)) {
2891
2924
  if (!url) continue;
@@ -2898,7 +2931,7 @@ async function runQAEngine(session, opts = {}) {
2898
2931
  }
2899
2932
  }
2900
2933
 
2901
- // ── Phase 10: Accessibility ──────────────────────────────────────────
2934
+ // Phase 10: Accessibility
2902
2935
  dash.setPhase('♿ Phase 10: Accessibility Check (WCAG 2.1)');
2903
2936
  const pageRoutes10 = session.routeMap.filter(r => r.type === 'page' || r.type === 'auth').slice(0, 10);
2904
2937
  for (const route of pageRoutes10) {
@@ -2915,7 +2948,7 @@ async function runQAEngine(session, opts = {}) {
2915
2948
  }
2916
2949
  }
2917
2950
 
2918
- // ── Phase 11: SEO ────────────────────────────────────────────────────
2951
+ // Phase 11: SEO
2919
2952
  dash.setPhase('🔎 Phase 11: SEO Validation (21 checks)');
2920
2953
  const seoRoutes = session.routeMap.filter(r => r.type === 'page').slice(0, 10);
2921
2954
  for (const route of seoRoutes) {
@@ -2929,7 +2962,7 @@ async function runQAEngine(session, opts = {}) {
2929
2962
  }
2930
2963
  }
2931
2964
 
2932
- // ── Phase 12: Load Test (v15) ────────────────────────────────────────
2965
+ // Phase 12: Load Test
2933
2966
  if (opts.loadTest !== false) {
2934
2967
  dash.setPhase('🔥 Phase 12: Load Testing');
2935
2968
  for (const [label, url] of Object.entries(session.urls)) {
@@ -2946,7 +2979,7 @@ async function runQAEngine(session, opts = {}) {
2946
2979
  }
2947
2980
  }
2948
2981
 
2949
- // ── Phase 13: HTTP Version (v15) ─────────────────────────────────────
2982
+ // Phase 13: HTTP Version
2950
2983
  dash.setPhase('🌐 Phase 13: HTTP Version & Protocol Check');
2951
2984
  for (const [label, url] of Object.entries(session.urls)) {
2952
2985
  if (!url) continue;
@@ -2956,7 +2989,7 @@ async function runQAEngine(session, opts = {}) {
2956
2989
  status: http.isHTTPS ? 'PASS' : 'FAIL', message: http.isHTTPS ? 'HTTPS in use' : 'HTTP only', url, label });
2957
2990
  }
2958
2991
 
2959
- // ── Phase 14: AI Classification ──────────────────────────────────────
2992
+ // Phase 14: AI Classification
2960
2993
  dash.setPhase('🤖 Phase 14: AI Bug Classification');
2961
2994
  dash.log(`Classifying ${session.bugs.length} bugs...`);
2962
2995
  for (const bug of session.bugs) {
@@ -3045,7 +3078,7 @@ async function saveToHistory(session, htmlPath, jsonPath) {
3045
3078
  }
3046
3079
 
3047
3080
  // ═══════════════════════════════════════════════════════════════════════════
3048
- // Public API — runUrlQA (main entry point)
3081
+ // Public API
3049
3082
  // ═══════════════════════════════════════════════════════════════════════════
3050
3083
  export async function runUrlQA({ localUrl, stagingUrl, prodUrl, loadTest = true } = {}) {
3051
3084
  const urls = {};