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.
- package/bin/index.js +3 -0
- package/package.json +1 -1
- package/src/qa/qa-engine.js +215 -182
package/src/qa/qa-engine.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// Backlist Enterprise QA Engine v15.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
|
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
|
|
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">💡
|
|
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
|
|
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
|
|
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 & 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
3081
|
+
// Public API
|
|
3049
3082
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3050
3083
|
export async function runUrlQA({ localUrl, stagingUrl, prodUrl, loadTest = true } = {}) {
|
|
3051
3084
|
const urls = {};
|