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.
- package/bin/index.js +1759 -315
- package/package.json +1 -1
- package/src/qa/qa-engine.js +84 -180
package/src/qa/qa-engine.js
CHANGED
|
@@ -76,7 +76,7 @@ async function getPlaywright() {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ─────────────────────────────────────────────────────────────────────────
|
|
79
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
|
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
|
|
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">💡
|
|
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
|
|
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
|
|
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 & 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
2952
|
+
// Public API
|
|
3049
2953
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3050
2954
|
export async function runUrlQA({ localUrl, stagingUrl, prodUrl, loadTest = true } = {}) {
|
|
3051
2955
|
const urls = {};
|