hauntview 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +358 -2
- package/VERSION +1 -0
- package/dist/cli.js +8184 -0
- package/package.json +120 -4
- package/scripts/postinstall.mjs +47 -0
- package/src/core/browser-launch.ts +60 -0
- package/src/core/browser-pool.ts +267 -0
- package/src/core/concurrency-resolver.ts +154 -0
- package/src/core/finding-classifier.ts +346 -0
- package/src/core/index.ts +8 -0
- package/src/core/profile-manager.ts +499 -0
- package/src/core/report-store.ts +117 -0
- package/src/core/screenshot-manager.ts +312 -0
- package/src/core/system-capability.ts +161 -0
- package/src/core/visual-diff.ts +230 -0
- package/src/engine.ts +473 -0
- package/src/profiles/accessibility-cognitive.ts +261 -0
- package/src/profiles/accessibility-keyboard.ts +225 -0
- package/src/profiles/accessibility-screenreader.ts +243 -0
- package/src/profiles/accessibility-visual.ts +257 -0
- package/src/profiles/broken-links.ts +261 -0
- package/src/profiles/browser-edge.ts +192 -0
- package/src/profiles/browser-firefox.ts +254 -0
- package/src/profiles/browser-legacy.ts +213 -0
- package/src/profiles/console-errors.ts +192 -0
- package/src/profiles/desktop-4k.ts +273 -0
- package/src/profiles/desktop.ts +192 -0
- package/src/profiles/font-failure.ts +148 -0
- package/src/profiles/form-resilience.ts +285 -0
- package/src/profiles/i18n-cjk.ts +218 -0
- package/src/profiles/i18n-mixed.ts +255 -0
- package/src/profiles/i18n-rtl.ts +236 -0
- package/src/profiles/image-failure.ts +178 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/js-disabled.ts +179 -0
- package/src/profiles/mobile.ts +175 -0
- package/src/profiles/network-resilience.ts +208 -0
- package/src/profiles/offline.ts +165 -0
- package/src/profiles/performance.ts +244 -0
- package/src/profiles/reduced-motion.ts +144 -0
- package/src/profiles/security-hygiene.ts +206 -0
- package/src/profiles/tablet.ts +176 -0
- package/src/reports/factory.ts +56 -0
- package/src/reports/html-generator.ts +1105 -0
- package/src/reports/index.ts +9 -0
- package/src/reports/markdown-generator.ts +349 -0
- package/src/reports/types.ts +34 -0
- package/src/reports/zip-generator.ts +85 -0
- package/src/types/contracts.ts +238 -0
- package/src/types/finding-utils.ts +121 -0
- package/src/types/index.ts +8 -0
- package/src/types/profiles.ts +175 -0
- package/src/types/results.ts +135 -0
- package/src/version.ts +5 -0
package/src/engine.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HauntView Testing Engine - Main orchestrator
|
|
3
|
+
* Coordinates browser pool, profile execution, and result collection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { chromium, webkit, firefox, Browser, Page } from 'playwright';
|
|
7
|
+
import { getChromiumLaunchOptions } from './core/browser-launch';
|
|
8
|
+
import { ProfileManager } from './core/profile-manager';
|
|
9
|
+
import { BrowserPool } from './core/browser-pool';
|
|
10
|
+
import { FindingClassifier } from './core/finding-classifier';
|
|
11
|
+
import { ScreenshotManager } from './core/screenshot-manager';
|
|
12
|
+
import {
|
|
13
|
+
TestExecutionOptions,
|
|
14
|
+
TestReport,
|
|
15
|
+
ProfileResult,
|
|
16
|
+
ReportGenerationOptions,
|
|
17
|
+
ProfileContext,
|
|
18
|
+
} from './types';
|
|
19
|
+
import { ReportFactory } from './reports/factory';
|
|
20
|
+
import { resolveConcurrency, executeWithConcurrency } from './core/concurrency-resolver';
|
|
21
|
+
import { MobileProfile } from './profiles/mobile';
|
|
22
|
+
import { DesktopProfile } from './profiles/desktop';
|
|
23
|
+
import { Desktop4KProfile } from './profiles/desktop-4k';
|
|
24
|
+
import { ConsoleErrorsProfile } from './profiles/console-errors';
|
|
25
|
+
import { TabletProfile } from './profiles/tablet';
|
|
26
|
+
import { PerformanceProfile } from './profiles/performance';
|
|
27
|
+
import { AccessibilityScreenReaderProfile } from './profiles/accessibility-screenreader';
|
|
28
|
+
import { AccessibilityKeyboardProfile } from './profiles/accessibility-keyboard';
|
|
29
|
+
import { AccessibilityVisualProfile } from './profiles/accessibility-visual';
|
|
30
|
+
import { AccessibilityCognitiveProfile } from './profiles/accessibility-cognitive';
|
|
31
|
+
import { I18nRTLProfile } from './profiles/i18n-rtl';
|
|
32
|
+
import { I18nCJKProfile } from './profiles/i18n-cjk';
|
|
33
|
+
import { I18nMixedProfile } from './profiles/i18n-mixed';
|
|
34
|
+
import { BrowserFirefoxProfile } from './profiles/browser-firefox';
|
|
35
|
+
import { BrowserEdgeProfile } from './profiles/browser-edge';
|
|
36
|
+
import { BrowserLegacyProfile } from './profiles/browser-legacy';
|
|
37
|
+
import { ImageFailureProfile } from './profiles/image-failure';
|
|
38
|
+
import { FontFailureProfile } from './profiles/font-failure';
|
|
39
|
+
import { JsDisabledProfile } from './profiles/js-disabled';
|
|
40
|
+
import { ReducedMotionProfile } from './profiles/reduced-motion';
|
|
41
|
+
import { OfflineProfile } from './profiles/offline';
|
|
42
|
+
import { BrokenLinksProfile } from './profiles/broken-links';
|
|
43
|
+
import { NetworkResilienceProfile } from './profiles/network-resilience';
|
|
44
|
+
import { SecurityHygieneProfile } from './profiles/security-hygiene';
|
|
45
|
+
import { FormResilienceProfile } from './profiles/form-resilience';
|
|
46
|
+
|
|
47
|
+
interface ExecutableProfile {
|
|
48
|
+
execute(page: Page, url: string, context?: ProfileContext): Promise<ProfileResult>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Profiles that manage their own BrowserContext and accept a Browser instance. */
|
|
52
|
+
export interface BrowserExecutableProfile {
|
|
53
|
+
execute(
|
|
54
|
+
browser: Browser,
|
|
55
|
+
url: string,
|
|
56
|
+
context?: ProfileContext,
|
|
57
|
+
screenshotManager?: ScreenshotManager
|
|
58
|
+
): Promise<ProfileResult>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class HauntViewEngine {
|
|
62
|
+
private profileManager: ProfileManager;
|
|
63
|
+
private browserPool: BrowserPool;
|
|
64
|
+
private classifier: FindingClassifier;
|
|
65
|
+
private screenshotManager: ScreenshotManager;
|
|
66
|
+
private browsers: Map<string, Browser> = new Map();
|
|
67
|
+
private browserLaunchPromises: Map<string, Promise<Browser>> = new Map();
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
this.profileManager = new ProfileManager();
|
|
71
|
+
this.browserPool = new BrowserPool({
|
|
72
|
+
maxBrowsers: 3,
|
|
73
|
+
maxPagesPerBrowser: 10,
|
|
74
|
+
idleTimeout: 30000,
|
|
75
|
+
});
|
|
76
|
+
this.classifier = new FindingClassifier();
|
|
77
|
+
this.screenshotManager = new ScreenshotManager('.hauntview/screenshots');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Thread-safe browser launcher/acquirer to avoid race conditions during concurrent runs
|
|
82
|
+
*/
|
|
83
|
+
private async getOrLaunchBrowser(browserType: string): Promise<Browser> {
|
|
84
|
+
const existing = this.browsers.get(browserType);
|
|
85
|
+
if (existing && existing.isConnected()) {
|
|
86
|
+
return existing;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.browserLaunchPromises.has(browserType)) {
|
|
90
|
+
return this.browserLaunchPromises.get(browserType)!;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const launchPromise = (async () => {
|
|
94
|
+
let launchedBrowser: Browser;
|
|
95
|
+
if (browserType === 'firefox') {
|
|
96
|
+
try {
|
|
97
|
+
launchedBrowser = await firefox.launch({ headless: true });
|
|
98
|
+
} catch {
|
|
99
|
+
const options = await getChromiumLaunchOptions();
|
|
100
|
+
launchedBrowser = await chromium.launch({
|
|
101
|
+
headless: true,
|
|
102
|
+
executablePath: options.executablePath,
|
|
103
|
+
args: options.args,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} else if (browserType === 'webkit') {
|
|
107
|
+
try {
|
|
108
|
+
launchedBrowser = await webkit.launch({ headless: true });
|
|
109
|
+
} catch {
|
|
110
|
+
const options = await getChromiumLaunchOptions();
|
|
111
|
+
launchedBrowser = await chromium.launch({
|
|
112
|
+
headless: true,
|
|
113
|
+
executablePath: options.executablePath,
|
|
114
|
+
args: options.args,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const options = await getChromiumLaunchOptions();
|
|
119
|
+
launchedBrowser = await chromium.launch({
|
|
120
|
+
headless: true,
|
|
121
|
+
executablePath: options.executablePath,
|
|
122
|
+
args: options.args,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.browsers.set(browserType, launchedBrowser);
|
|
127
|
+
this.browserPool.registerBrowser(browserType, launchedBrowser);
|
|
128
|
+
return launchedBrowser;
|
|
129
|
+
})().finally(() => {
|
|
130
|
+
this.browserLaunchPromises.delete(browserType);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.browserLaunchPromises.set(browserType, launchPromise);
|
|
134
|
+
return launchPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Initialize the engine
|
|
139
|
+
*/
|
|
140
|
+
async initialize(): Promise<void> {
|
|
141
|
+
await this.screenshotManager.initialize();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute test suite
|
|
146
|
+
*/
|
|
147
|
+
async executeTests(options: TestExecutionOptions): Promise<TestReport> {
|
|
148
|
+
const reportId = `hauntview-${Date.now()}`;
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
|
|
151
|
+
// Ensure screenshot directory is ready
|
|
152
|
+
await this.screenshotManager.initialize();
|
|
153
|
+
|
|
154
|
+
// Determine which profiles to run
|
|
155
|
+
let executionOrder = this.profileManager.getOptimalExecutionOrder();
|
|
156
|
+
if (options.profiles && options.profiles.length > 0) {
|
|
157
|
+
executionOrder = executionOrder.filter((p) => options.profiles!.includes(p.id));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Resolve concurrency settings & apply hosted guards
|
|
161
|
+
const resolved = resolveConcurrency(options);
|
|
162
|
+
if (resolved.downgradedDueToHosted) {
|
|
163
|
+
console.warn(
|
|
164
|
+
'[engine] Hosted environment detected (Render / HAUNT_HOSTED); concurrency was requested but downgraded to sequential to protect host resources.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const profileResults: ProfileResult[] = [];
|
|
169
|
+
|
|
170
|
+
if (!resolved.concurrency || resolved.concurrencyAmount <= 1) {
|
|
171
|
+
// Sequential execution (original path)
|
|
172
|
+
for (const profile of executionOrder) {
|
|
173
|
+
try {
|
|
174
|
+
const context = this.profileManager.createExecutionContext(profile.id);
|
|
175
|
+
if (!context) continue;
|
|
176
|
+
|
|
177
|
+
const result = await this.executeProfile(profile.id, options.url, context);
|
|
178
|
+
profileResults.push(result);
|
|
179
|
+
|
|
180
|
+
// Classify findings
|
|
181
|
+
for (const finding of result.findings) {
|
|
182
|
+
const classified = await this.classifier.classify(finding);
|
|
183
|
+
finding.canonicalHash = classified.canonical.canonicalHash;
|
|
184
|
+
finding.isClassified = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.profileManager.recordExecution(profile.id, result.duration);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`Error executing profile ${profile.id}:`, error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// Concurrent execution with bounded pool and deterministic result ordering
|
|
194
|
+
const tasks = executionOrder.map((profile) => async () => {
|
|
195
|
+
const context = this.profileManager.createExecutionContext(profile.id);
|
|
196
|
+
if (!context) return null;
|
|
197
|
+
|
|
198
|
+
const result = await this.executeProfile(profile.id, options.url, context);
|
|
199
|
+
|
|
200
|
+
for (const finding of result.findings) {
|
|
201
|
+
const classified = await this.classifier.classify(finding);
|
|
202
|
+
finding.canonicalHash = classified.canonical.canonicalHash;
|
|
203
|
+
finding.isClassified = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.profileManager.recordExecution(profile.id, result.duration);
|
|
207
|
+
return result;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const rawResults = await executeWithConcurrency(tasks, resolved.concurrencyAmount, {
|
|
211
|
+
onFailure: (index, error) => {
|
|
212
|
+
const profile = executionOrder[index];
|
|
213
|
+
console.error(`Error executing profile ${profile.id}:`, error);
|
|
214
|
+
const failedResult: ProfileResult = {
|
|
215
|
+
profileId: profile.id,
|
|
216
|
+
profileName: profile.name,
|
|
217
|
+
success: false,
|
|
218
|
+
startTime: Date.now(),
|
|
219
|
+
endTime: Date.now(),
|
|
220
|
+
duration: 0,
|
|
221
|
+
findings: [],
|
|
222
|
+
screenshots: [],
|
|
223
|
+
error: {
|
|
224
|
+
message: error instanceof Error ? error.message : String(error),
|
|
225
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
226
|
+
},
|
|
227
|
+
metadata: {
|
|
228
|
+
browserEngine: 'playwright',
|
|
229
|
+
browserVersion: 'unknown',
|
|
230
|
+
nodeVersion: process.version,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
return failedResult;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
for (const res of rawResults) {
|
|
238
|
+
if (res) {
|
|
239
|
+
profileResults.push(res);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const endTime = Date.now();
|
|
245
|
+
|
|
246
|
+
// Aggregate results
|
|
247
|
+
const testReport = this.aggregateResults(
|
|
248
|
+
reportId,
|
|
249
|
+
options.url,
|
|
250
|
+
startTime,
|
|
251
|
+
endTime,
|
|
252
|
+
profileResults
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return testReport;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Execute a single profile
|
|
260
|
+
*/
|
|
261
|
+
private async executeProfile(
|
|
262
|
+
profileId: string,
|
|
263
|
+
url: string,
|
|
264
|
+
context: ProfileContext
|
|
265
|
+
): Promise<ProfileResult> {
|
|
266
|
+
let browserType = 'chromium';
|
|
267
|
+
if (profileId === 'browser-webkit') {
|
|
268
|
+
browserType = 'webkit';
|
|
269
|
+
} else if (profileId === 'browser-firefox') {
|
|
270
|
+
browserType = 'firefox';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const browser = await this.getOrLaunchBrowser(browserType);
|
|
274
|
+
|
|
275
|
+
const page = await this.browserPool.acquirePage(browserType, context, browser);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
let profile: ExecutableProfile | undefined;
|
|
279
|
+
let result: ProfileResult | undefined;
|
|
280
|
+
|
|
281
|
+
// Device profiles
|
|
282
|
+
if (
|
|
283
|
+
profileId === 'mobile-iphone15' ||
|
|
284
|
+
profileId === 'mobile-android' ||
|
|
285
|
+
profileId === 'mobile'
|
|
286
|
+
) {
|
|
287
|
+
profile = new MobileProfile(profileId, 'Mobile');
|
|
288
|
+
} else if (profileId === 'tablet-ipad' || profileId === 'tablet') {
|
|
289
|
+
profile = new TabletProfile(profileId, 'Tablet');
|
|
290
|
+
} else if (profileId === 'desktop-4k') {
|
|
291
|
+
profile = new Desktop4KProfile(profileId, 'Desktop - 4K');
|
|
292
|
+
} else if (profileId === 'desktop-1080p' || profileId === 'desktop') {
|
|
293
|
+
profile = new DesktopProfile(profileId, 'Desktop');
|
|
294
|
+
|
|
295
|
+
// Console errors profile
|
|
296
|
+
} else if (profileId === 'console-errors') {
|
|
297
|
+
profile = new ConsoleErrorsProfile(profileId, 'Console Errors');
|
|
298
|
+
|
|
299
|
+
// Performance profile
|
|
300
|
+
} else if (profileId === 'performance') {
|
|
301
|
+
profile = new PerformanceProfile(profileId, 'Performance');
|
|
302
|
+
|
|
303
|
+
// Accessibility profiles
|
|
304
|
+
} else if (profileId === 'a11y-screenreader') {
|
|
305
|
+
profile = new AccessibilityScreenReaderProfile(profileId, 'Accessibility - Screen Reader');
|
|
306
|
+
} else if (profileId === 'a11y-keyboard') {
|
|
307
|
+
profile = new AccessibilityKeyboardProfile(profileId, 'Accessibility - Keyboard');
|
|
308
|
+
} else if (profileId === 'a11y-visual') {
|
|
309
|
+
profile = new AccessibilityVisualProfile(profileId, 'Accessibility - Visual');
|
|
310
|
+
} else if (profileId === 'a11y-cognitive') {
|
|
311
|
+
profile = new AccessibilityCognitiveProfile(profileId, 'Accessibility - Cognitive');
|
|
312
|
+
|
|
313
|
+
// Internationalization profiles
|
|
314
|
+
} else if (profileId === 'i18n-rtl') {
|
|
315
|
+
profile = new I18nRTLProfile(profileId, 'I18N - RTL (Arabic)');
|
|
316
|
+
} else if (profileId === 'i18n-cjk') {
|
|
317
|
+
profile = new I18nCJKProfile(profileId, 'I18N - CJK (Japanese)');
|
|
318
|
+
} else if (profileId === 'i18n-mixed') {
|
|
319
|
+
profile = new I18nMixedProfile(profileId, 'I18N - Mixed Scripts');
|
|
320
|
+
|
|
321
|
+
// Browser compatibility profiles
|
|
322
|
+
} else if (profileId === 'browser-firefox') {
|
|
323
|
+
profile = new BrowserFirefoxProfile(profileId, 'Browser - Firefox');
|
|
324
|
+
} else if (profileId === 'browser-edge') {
|
|
325
|
+
profile = new BrowserEdgeProfile(profileId, 'Browser - Edge');
|
|
326
|
+
} else if (profileId === 'browser-legacy') {
|
|
327
|
+
profile = new BrowserLegacyProfile(profileId, 'Browser - Legacy');
|
|
328
|
+
} else if (profileId.startsWith('browser-')) {
|
|
329
|
+
profile = new ConsoleErrorsProfile(profileId, `Browser - ${profileId}`);
|
|
330
|
+
|
|
331
|
+
// Canonical rites — Page-based
|
|
332
|
+
} else if (profileId === 'image-failure') {
|
|
333
|
+
profile = new ImageFailureProfile(profileId, 'Image Failure');
|
|
334
|
+
} else if (profileId === 'font-failure') {
|
|
335
|
+
profile = new FontFailureProfile(profileId, 'Font Failure');
|
|
336
|
+
} else if (profileId === 'broken-links') {
|
|
337
|
+
profile = new BrokenLinksProfile(profileId, 'Broken Links');
|
|
338
|
+
} else if (profileId === 'security-hygiene') {
|
|
339
|
+
profile = new SecurityHygieneProfile(profileId, 'Security Hygiene');
|
|
340
|
+
|
|
341
|
+
// Canonical rites — Browser-based (manage their own context)
|
|
342
|
+
} else if (profileId === 'js-disabled') {
|
|
343
|
+
const p = new JsDisabledProfile(profileId, 'JS Disabled');
|
|
344
|
+
result = await p.execute(browser, url, context, this.screenshotManager);
|
|
345
|
+
} else if (profileId === 'reduced-motion') {
|
|
346
|
+
const p = new ReducedMotionProfile(profileId, 'Reduced Motion');
|
|
347
|
+
result = await p.execute(browser, url, context, this.screenshotManager);
|
|
348
|
+
} else if (profileId === 'offline') {
|
|
349
|
+
const p = new OfflineProfile(profileId, 'Offline');
|
|
350
|
+
result = await p.execute(browser, url, context, this.screenshotManager);
|
|
351
|
+
} else if (profileId === 'network-resilience') {
|
|
352
|
+
const p = new NetworkResilienceProfile(profileId, 'Network Resilience');
|
|
353
|
+
result = await p.execute(browser, url, context, this.screenshotManager);
|
|
354
|
+
} else if (profileId === 'form-resilience') {
|
|
355
|
+
const p = new FormResilienceProfile(profileId, 'Form Resilience');
|
|
356
|
+
result = await p.execute(browser, url, context, this.screenshotManager);
|
|
357
|
+
} else {
|
|
358
|
+
// Default fallback
|
|
359
|
+
profile = new ConsoleErrorsProfile(profileId, profileId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!result) {
|
|
363
|
+
result = await profile!.execute(page, url, context);
|
|
364
|
+
|
|
365
|
+
// Capture actual apparition screenshot plate
|
|
366
|
+
try {
|
|
367
|
+
const screenshotMeta = await this.screenshotManager.captureScreenshot(
|
|
368
|
+
page,
|
|
369
|
+
profileId,
|
|
370
|
+
url,
|
|
371
|
+
context.viewport,
|
|
372
|
+
{ fullPage: false }
|
|
373
|
+
);
|
|
374
|
+
result.screenshots = [screenshotMeta];
|
|
375
|
+
|
|
376
|
+
// Attach visual evidence to findings
|
|
377
|
+
for (const finding of result.findings) {
|
|
378
|
+
if (!finding.details) finding.details = {};
|
|
379
|
+
finding.details.screenshot = screenshotMeta.fileName;
|
|
380
|
+
}
|
|
381
|
+
} catch (screenshotErr) {
|
|
382
|
+
console.warn(`Could not capture screenshot for profile ${profileId}:`, screenshotErr);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return result;
|
|
387
|
+
} finally {
|
|
388
|
+
this.browserPool.releasePage(page);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Aggregate profile results into test report
|
|
394
|
+
*/
|
|
395
|
+
private aggregateResults(
|
|
396
|
+
reportId: string,
|
|
397
|
+
url: string,
|
|
398
|
+
startTime: number,
|
|
399
|
+
endTime: number,
|
|
400
|
+
profileResults: ProfileResult[]
|
|
401
|
+
): TestReport {
|
|
402
|
+
// Deduplicate findings
|
|
403
|
+
const allFindings = profileResults.flatMap((r) => r.findings);
|
|
404
|
+
const deduplicatedFindings = this.classifier.deduplicateFindings(allFindings);
|
|
405
|
+
|
|
406
|
+
// Calculate summary
|
|
407
|
+
const findingsBySeverity = {
|
|
408
|
+
error: deduplicatedFindings.filter((f) => f.severity === 'error').length,
|
|
409
|
+
warning: deduplicatedFindings.filter((f) => f.severity === 'warning').length,
|
|
410
|
+
info: deduplicatedFindings.filter((f) => f.severity === 'info').length,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const findingsByCategory = new Map<string, number>();
|
|
414
|
+
for (const finding of deduplicatedFindings) {
|
|
415
|
+
const category = finding.category || 'uncategorized';
|
|
416
|
+
findingsByCategory.set(category, (findingsByCategory.get(category) || 0) + 1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
id: reportId,
|
|
421
|
+
url,
|
|
422
|
+
startTime,
|
|
423
|
+
endTime,
|
|
424
|
+
duration: endTime - startTime,
|
|
425
|
+
profileResults,
|
|
426
|
+
summary: {
|
|
427
|
+
totalProfiles: profileResults.length,
|
|
428
|
+
successfulProfiles: profileResults.filter((r) => r.success).length,
|
|
429
|
+
failedProfiles: profileResults.filter((r) => !r.success).length,
|
|
430
|
+
totalFindings: deduplicatedFindings.length,
|
|
431
|
+
findingsBySeverity,
|
|
432
|
+
findingsByCategory: Object.fromEntries(findingsByCategory),
|
|
433
|
+
canonicalizedFindings: deduplicatedFindings.filter((f) => f.canonicalHash).length,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Clean up resources
|
|
440
|
+
*/
|
|
441
|
+
async cleanup(): Promise<void> {
|
|
442
|
+
await this.browserPool.closeAll();
|
|
443
|
+
|
|
444
|
+
for (const browser of this.browsers.values()) {
|
|
445
|
+
await browser.close();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.browsers.clear();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Generate report from test results
|
|
453
|
+
*/
|
|
454
|
+
async generateReport(report: TestReport, options: ReportGenerationOptions) {
|
|
455
|
+
const generator = ReportFactory.createGenerator(options.format);
|
|
456
|
+
return generator.generate(report, {
|
|
457
|
+
format: options.format as 'html' | 'markdown' | 'json',
|
|
458
|
+
includeScreenshots: options.includeScreenshots ?? true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get engine statistics
|
|
464
|
+
*/
|
|
465
|
+
getStats() {
|
|
466
|
+
return {
|
|
467
|
+
browserPool: this.browserPool.getStats(),
|
|
468
|
+
classifier: this.classifier.getStats(),
|
|
469
|
+
screenshots: this.screenshotManager.getStats(),
|
|
470
|
+
profiles: this.profileManager.getProfileCount(),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|