docs: enterprise refactor plan with ralph specs
This commit is contained in:
501
dist/plugins/agents/PlaywrightAgent.js
vendored
Normal file
501
dist/plugins/agents/PlaywrightAgent.js
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
"use strict";
|
||||
/**
|
||||
* PlaywrightAgent — implements IInteractionAgent using Playwright.
|
||||
* All random choices use a deterministic seed and are logged.
|
||||
* Supports scope enforcement, auth injection, and action delay.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PlaywrightAgent = void 0;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const playwright_1 = require("playwright");
|
||||
const Logger_1 = require("../../core/Logger");
|
||||
const ExplorationConfig_1 = require("../../core/ExplorationConfig");
|
||||
/** Simple deterministic pseudo-random number generator (LCG) */
|
||||
class SeededRandom {
|
||||
constructor(seed) {
|
||||
this.state = seed;
|
||||
}
|
||||
/** Returns a float in [0, 1) */
|
||||
next() {
|
||||
this.state = (this.state * 1664525 + 1013904223) & 0xffffffff;
|
||||
return (this.state >>> 0) / 0x100000000;
|
||||
}
|
||||
/** Returns an integer in [0, max) */
|
||||
nextInt(max) {
|
||||
return Math.floor(this.next() * max);
|
||||
}
|
||||
}
|
||||
function generateId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
function domHash(url, domSnapshot) {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update(url + domSnapshot)
|
||||
.digest('hex')
|
||||
.substring(0, 16);
|
||||
}
|
||||
class PlaywrightAgent {
|
||||
constructor(config = {}) {
|
||||
/** Captured HTTP responses for the current action */
|
||||
this.pendingResponses = [];
|
||||
this.pendingConsoleErrors = [];
|
||||
this.pendingJsExceptions = [];
|
||||
this.seed = config.seed ?? 42;
|
||||
this.rng = new SeededRandom(this.seed);
|
||||
this.headless = config.headless ?? true;
|
||||
this.timeoutMs = config.timeoutMs ?? 30000;
|
||||
this.logger = config.logger ?? new Logger_1.NullLogger();
|
||||
this.explorationConfig = config.explorationConfig ?? {};
|
||||
}
|
||||
async launch(url) {
|
||||
// Select browser type
|
||||
const browserType = this.explorationConfig.browsers?.[0] ?? 'chromium';
|
||||
const launcher = browserType === 'firefox' ? playwright_1.firefox : browserType === 'webkit' ? playwright_1.webkit : playwright_1.chromium;
|
||||
this.browser = await launcher.launch({ headless: this.headless });
|
||||
// Apply auth headers if configured
|
||||
const auth = this.explorationConfig.auth;
|
||||
let contextOptions = {};
|
||||
// Mobile device emulation
|
||||
const mobileDevice = this.explorationConfig.mobileDevice;
|
||||
if (mobileDevice && mobileDevice !== 'none') {
|
||||
const device = playwright_1.devices[mobileDevice];
|
||||
if (device) {
|
||||
contextOptions = { ...device, ...contextOptions };
|
||||
}
|
||||
}
|
||||
// Custom viewport
|
||||
if (this.explorationConfig.viewport) {
|
||||
contextOptions.viewport = this.explorationConfig.viewport;
|
||||
}
|
||||
if (auth?.type === 'headers') {
|
||||
contextOptions.extraHTTPHeaders = auth.headers;
|
||||
}
|
||||
this.context = await this.browser.newContext(contextOptions);
|
||||
// Apply auth cookies if configured
|
||||
if (auth?.type === 'cookies') {
|
||||
await this.context.addCookies(auth.cookies);
|
||||
}
|
||||
this.page = await this.context.newPage();
|
||||
this.setupListeners(this.page);
|
||||
// Apply network chaos conditions
|
||||
await this.applyNetworkChaos(this.page);
|
||||
// Login flow auth
|
||||
if (auth?.type === 'login_flow') {
|
||||
await this.performLoginFlow(auth);
|
||||
}
|
||||
await this.page.goto(url, { timeout: this.timeoutMs });
|
||||
}
|
||||
async close() {
|
||||
await this.browser?.close();
|
||||
this.browser = undefined;
|
||||
this.context = undefined;
|
||||
this.page = undefined;
|
||||
}
|
||||
async captureState() {
|
||||
const page = this.requirePage();
|
||||
const url = page.url();
|
||||
const title = await page.title();
|
||||
const domSnapshot = await page.evaluate(() => document.body.outerHTML);
|
||||
const stateId = domHash(url, domSnapshot);
|
||||
return {
|
||||
id: stateId,
|
||||
url,
|
||||
title,
|
||||
timestamp: Date.now(),
|
||||
domSnapshot,
|
||||
visitCount: 0,
|
||||
};
|
||||
}
|
||||
async discoverActions(state) {
|
||||
const page = this.requirePage();
|
||||
const actions = [];
|
||||
const now = Date.now();
|
||||
const currentUrl = page.url();
|
||||
if (this.isExcludedPath(currentUrl)) {
|
||||
return [];
|
||||
}
|
||||
// Discover clickable elements
|
||||
const clickableSelectors = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'[role="button"]',
|
||||
'input[type="submit"]',
|
||||
'input[type="button"]',
|
||||
];
|
||||
for (const selector of clickableSelectors) {
|
||||
if (this.isExcludedSelector(selector))
|
||||
continue;
|
||||
const elements = await page.locator(selector).all();
|
||||
for (const el of elements) {
|
||||
const isVisible = await el.isVisible().catch(() => false);
|
||||
if (!isVisible)
|
||||
continue;
|
||||
// Check element against excluded selectors
|
||||
const elSel = await this.buildSelector(el, selector);
|
||||
if (this.isExcludedSelector(elSel))
|
||||
continue;
|
||||
// For links, check domain is allowed
|
||||
if (selector === 'a[href]') {
|
||||
const href = await el.getAttribute('href').catch(() => null);
|
||||
if (href && this.isExternalLink(href, currentUrl))
|
||||
continue;
|
||||
}
|
||||
const actionSeed = this.rng.nextInt(0x7fffffff);
|
||||
actions.push({
|
||||
id: generateId(),
|
||||
type: 'click',
|
||||
selector: elSel,
|
||||
timestamp: now,
|
||||
seed: actionSeed,
|
||||
stateId: state.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Discover fillable inputs
|
||||
const inputSelectors = [
|
||||
'input[type="text"]',
|
||||
'input[type="email"]',
|
||||
'input[type="password"]',
|
||||
'textarea',
|
||||
];
|
||||
for (const selector of inputSelectors) {
|
||||
if (this.isExcludedSelector(selector))
|
||||
continue;
|
||||
const elements = await page.locator(selector).all();
|
||||
for (const el of elements) {
|
||||
const isVisible = await el.isVisible().catch(() => false);
|
||||
if (!isVisible)
|
||||
continue;
|
||||
const elSel = await this.buildSelector(el, selector);
|
||||
if (this.isExcludedSelector(elSel))
|
||||
continue;
|
||||
const actionSeed = this.rng.nextInt(0x7fffffff);
|
||||
actions.push({
|
||||
id: generateId(),
|
||||
type: 'fill',
|
||||
selector: elSel,
|
||||
value: '',
|
||||
timestamp: now,
|
||||
seed: actionSeed,
|
||||
stateId: state.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
async executeAction(action) {
|
||||
const page = this.requirePage();
|
||||
this.resetPending();
|
||||
const observationId = generateId();
|
||||
const actionDelayMs = this.explorationConfig.actionDelayMs ?? 0;
|
||||
// Skip actions targeting excluded paths
|
||||
if (action.url && this.isExcludedPath(action.url)) {
|
||||
const state = await this.captureState();
|
||||
return this.buildObservation(observationId, action.id, state.id);
|
||||
}
|
||||
// Enforce allowed domains for navigate actions
|
||||
if (action.type === 'navigate' && action.url) {
|
||||
if (!this.isAllowedUrl(action.url)) {
|
||||
const state = await this.captureState();
|
||||
return this.buildObservation(observationId, action.id, state.id);
|
||||
}
|
||||
}
|
||||
try {
|
||||
switch (action.type) {
|
||||
case 'click':
|
||||
await page.locator(action.selector).first().click({ timeout: this.timeoutMs });
|
||||
break;
|
||||
case 'fill':
|
||||
await page.locator(action.selector).first().fill(action.value ?? '', { timeout: this.timeoutMs });
|
||||
break;
|
||||
case 'navigate':
|
||||
await page.goto(action.url, { timeout: this.timeoutMs });
|
||||
break;
|
||||
case 'submit':
|
||||
await page.locator(action.selector).first().dispatchEvent('submit');
|
||||
break;
|
||||
case 'select':
|
||||
await page.locator(action.selector).first().selectOption(action.value ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
this.pendingJsExceptions.push(`Action ${action.type} failed: ${msg}`);
|
||||
}
|
||||
// Wait for async effects to settle + configured delay
|
||||
await page.waitForTimeout(200 + actionDelayMs);
|
||||
const newState = await this.captureState();
|
||||
return this.buildObservation(observationId, action.id, newState.id);
|
||||
}
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────
|
||||
getPage() {
|
||||
return this.requirePage();
|
||||
}
|
||||
requirePage() {
|
||||
if (!this.page)
|
||||
throw new Error('PlaywrightAgent: not launched. Call launch() first.');
|
||||
return this.page;
|
||||
}
|
||||
resetPending() {
|
||||
this.pendingResponses = [];
|
||||
this.pendingConsoleErrors = [];
|
||||
this.pendingJsExceptions = [];
|
||||
}
|
||||
buildObservation(observationId, actionId, newStateId) {
|
||||
return {
|
||||
id: observationId,
|
||||
actionId,
|
||||
newStateId,
|
||||
httpResponses: [...this.pendingResponses],
|
||||
consoleErrors: [...this.pendingConsoleErrors],
|
||||
jsExceptions: [...this.pendingJsExceptions],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
setupListeners(page) {
|
||||
const requestTimestamps = new Map();
|
||||
page.on('request', (req) => {
|
||||
requestTimestamps.set(req.url(), Date.now());
|
||||
});
|
||||
page.on('response', (res) => {
|
||||
const start = requestTimestamps.get(res.url()) ?? Date.now();
|
||||
const durationMs = Date.now() - start;
|
||||
this.pendingResponses.push({
|
||||
url: res.url(),
|
||||
status: res.status(),
|
||||
method: res.request().method(),
|
||||
durationMs,
|
||||
});
|
||||
});
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
this.pendingConsoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
page.on('pageerror', (err) => {
|
||||
this.pendingJsExceptions.push(err.message);
|
||||
});
|
||||
}
|
||||
async buildSelector(el, fallback) {
|
||||
try {
|
||||
const id = await el.getAttribute('id').catch(() => null);
|
||||
if (id)
|
||||
return `#${id}`;
|
||||
const name = await el.getAttribute('name').catch(() => null);
|
||||
if (name)
|
||||
return `[name="${name}"]`;
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
isExcludedPath(urlOrPath) {
|
||||
const excludedPaths = this.explorationConfig.excludedPaths ?? [];
|
||||
if (excludedPaths.length === 0)
|
||||
return false;
|
||||
try {
|
||||
const parsed = new URL(urlOrPath, 'http://placeholder');
|
||||
return excludedPaths.some((p) => parsed.pathname.startsWith(p));
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
isExcludedSelector(selector) {
|
||||
const excludedSelectors = this.explorationConfig.excludedSelectors ?? [];
|
||||
return excludedSelectors.includes(selector);
|
||||
}
|
||||
isExternalLink(href, currentUrl) {
|
||||
const allowedDomains = this.explorationConfig.allowedDomains ?? [];
|
||||
if (allowedDomains.length === 0)
|
||||
return false;
|
||||
try {
|
||||
const base = new URL(currentUrl);
|
||||
const target = new URL(href, base.origin);
|
||||
return !allowedDomains.includes(target.hostname);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
isAllowedUrl(url) {
|
||||
const allowedDomains = this.explorationConfig.allowedDomains ?? [];
|
||||
if (allowedDomains.length === 0)
|
||||
return true;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return allowedDomains.includes(parsed.hostname);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async performLoginFlow(auth) {
|
||||
const page = this.requirePage();
|
||||
await page.goto(auth.loginUrl, { timeout: this.timeoutMs });
|
||||
await page.locator(auth.usernameSelector).first().fill(auth.username);
|
||||
await page.locator(auth.passwordSelector).first().fill(auth.password);
|
||||
await page.locator(auth.submitSelector).first().click();
|
||||
await page.waitForNavigation({ timeout: this.timeoutMs }).catch(() => undefined);
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl === auth.loginUrl || currentUrl.includes(new URL(auth.loginUrl).pathname)) {
|
||||
throw new Error(`Login failed: still on login page ${currentUrl}`);
|
||||
}
|
||||
}
|
||||
async applyNetworkChaos(page) {
|
||||
const chaos = this.explorationConfig.networkChaos;
|
||||
if (!chaos?.enabled)
|
||||
return;
|
||||
// Apply network condition via CDP (Chromium only)
|
||||
const profile = chaos.profile ?? 'none';
|
||||
const condition = ExplorationConfig_1.NETWORK_PROFILES[profile];
|
||||
if (condition) {
|
||||
try {
|
||||
const client = await this.context.newCDPSession(page);
|
||||
await client.send('Network.emulateNetworkConditions', {
|
||||
offline: condition.offline,
|
||||
downloadThroughput: condition.offline ? -1 : (condition.downloadKbps * 1024) / 8,
|
||||
uploadThroughput: condition.offline ? -1 : (condition.uploadKbps * 1024) / 8,
|
||||
latency: condition.latencyMs,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
// CDP not available (e.g. non-Chromium browser) — ignore
|
||||
}
|
||||
}
|
||||
// Block specified endpoints
|
||||
if (chaos.blockedEndpoints && chaos.blockedEndpoints.length > 0) {
|
||||
await page.route('**/*', (route) => {
|
||||
const url = route.request().url();
|
||||
const isBlocked = chaos.blockedEndpoints.some((pattern) => this.matchGlob(url, pattern));
|
||||
if (isBlocked) {
|
||||
route.fulfill({ status: 503, body: 'Service Unavailable (ABE Network Chaos)' });
|
||||
}
|
||||
else {
|
||||
route.continue();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Slow down specified endpoints
|
||||
if (chaos.slowEndpoints && chaos.slowEndpoints.length > 0) {
|
||||
for (const slowEp of chaos.slowEndpoints) {
|
||||
await page.route(slowEp.pattern, async (route) => {
|
||||
await new Promise((r) => setTimeout(r, slowEp.delayMs));
|
||||
route.continue();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
matchGlob(url, pattern) {
|
||||
// Convert glob pattern to regex
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
try {
|
||||
return new RegExp(escaped).test(url);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Detects mobile layout issues on the current page:
|
||||
* - Touch targets smaller than 44x44px (WCAG 2.5.5 / Apple HIG)
|
||||
* - Horizontal content overflow beyond viewport width
|
||||
*/
|
||||
async detectMobileLayoutIssues(stateId, sessionId, actionTrace) {
|
||||
const page = this.requirePage();
|
||||
const anomalies = [];
|
||||
try {
|
||||
const issues = await page.evaluate(() => {
|
||||
const findings = [];
|
||||
// Check for horizontal overflow
|
||||
const docWidth = document.documentElement.scrollWidth;
|
||||
const viewportWidth = window.innerWidth;
|
||||
if (docWidth > viewportWidth) {
|
||||
findings.push(`horizontal_overflow: ${docWidth}px > ${viewportWidth}px viewport`);
|
||||
}
|
||||
// Check for small touch targets (< 44x44 px)
|
||||
const interactiveSelectors = ['a', 'button', '[role="button"]', 'input', 'select', 'textarea'];
|
||||
const seen = new Set();
|
||||
for (const sel of interactiveSelectors) {
|
||||
for (const el of document.querySelectorAll(sel)) {
|
||||
if (seen.has(el))
|
||||
continue;
|
||||
seen.add(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 && rect.height === 0)
|
||||
continue;
|
||||
if (rect.width < 44 || rect.height < 44) {
|
||||
const label = el.getAttribute('aria-label') ||
|
||||
el.textContent?.trim().slice(0, 30) ||
|
||||
el.tagName.toLowerCase();
|
||||
findings.push(`small_touch_target: "${label}" (${Math.round(rect.width)}x${Math.round(rect.height)}px)`);
|
||||
if (findings.filter((f) => f.startsWith('small_touch_target')).length >= 5)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (findings.filter((f) => f.startsWith('small_touch_target')).length >= 5)
|
||||
break;
|
||||
}
|
||||
return findings;
|
||||
}).catch(() => []);
|
||||
if (issues.length === 0)
|
||||
return anomalies;
|
||||
const hasOverflow = issues.some((i) => i.startsWith('horizontal_overflow'));
|
||||
const smallTargetCount = issues.filter((i) => i.startsWith('small_touch_target')).length;
|
||||
const severity = hasOverflow ? 'high' : smallTargetCount >= 3 ? 'medium' : 'low';
|
||||
anomalies.push({
|
||||
id: generateId(),
|
||||
type: 'mobile_layout_issue',
|
||||
severity,
|
||||
observationId: stateId,
|
||||
actionTrace,
|
||||
description: `Mobile layout issues detected: ${issues.slice(0, 3).join('; ')}`,
|
||||
evidence: { rawErrors: issues },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
catch {
|
||||
// Page may be in invalid state — ignore
|
||||
}
|
||||
return anomalies;
|
||||
}
|
||||
}
|
||||
exports.PlaywrightAgent = PlaywrightAgent;
|
||||
124
dist/plugins/collectors/AccessibilityCollector.js
vendored
Normal file
124
dist/plugins/collectors/AccessibilityCollector.js
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
"use strict";
|
||||
/**
|
||||
* AccessibilityCollector — runs axe-core after state changes to detect WCAG violations.
|
||||
* Converts axe violations to IAnomaly with severity mapped from impact level.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AccessibilityCollector = exports.DEFAULT_A11Y_CONFIG = void 0;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
exports.DEFAULT_A11Y_CONFIG = {
|
||||
enabled: true,
|
||||
minImpact: 'serious',
|
||||
wcagLevel: 'AA',
|
||||
};
|
||||
const IMPACT_TO_SEVERITY = {
|
||||
minor: 'low',
|
||||
moderate: 'medium',
|
||||
serious: 'high',
|
||||
critical: 'critical',
|
||||
};
|
||||
const IMPACT_RANK = {
|
||||
minor: 0,
|
||||
moderate: 1,
|
||||
serious: 2,
|
||||
critical: 3,
|
||||
};
|
||||
class AccessibilityCollector {
|
||||
constructor(config = {}) {
|
||||
this.config = { ...exports.DEFAULT_A11Y_CONFIG, ...config };
|
||||
}
|
||||
async collect(page, stateId, sessionId, actionTrace) {
|
||||
if (!this.config.enabled)
|
||||
return [];
|
||||
try {
|
||||
const violations = await this.runAxe(page);
|
||||
const minRank = IMPACT_RANK[this.config.minImpact] ?? 2;
|
||||
const anomalies = [];
|
||||
for (const violation of violations) {
|
||||
const impact = violation.impact ?? 'minor';
|
||||
if ((IMPACT_RANK[impact] ?? 0) < minRank)
|
||||
continue;
|
||||
const severity = IMPACT_TO_SEVERITY[impact] ?? 'medium';
|
||||
anomalies.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'accessibility_violation',
|
||||
severity,
|
||||
observationId: stateId,
|
||||
actionTrace,
|
||||
description: `[axe] ${violation.description}`,
|
||||
evidence: {
|
||||
rawErrors: [
|
||||
`Rule: ${violation.id}`,
|
||||
`Impact: ${impact}`,
|
||||
`Affected nodes: ${violation.nodes.length}`,
|
||||
`Help: ${violation.helpUrl}`,
|
||||
],
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
return anomalies;
|
||||
}
|
||||
catch {
|
||||
// axe might fail if page is not in a valid state
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async runAxe(page) {
|
||||
try {
|
||||
const { AxeBuilder } = await Promise.resolve().then(() => __importStar(require('@axe-core/playwright')));
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa'])
|
||||
.analyze();
|
||||
return results.violations;
|
||||
}
|
||||
catch {
|
||||
// Fallback: try via page.evaluate if AxeBuilder fails
|
||||
const results = await page.evaluate(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const win = window;
|
||||
if (typeof win.axe === 'undefined')
|
||||
return [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const r = await win.axe.run();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return r.violations;
|
||||
}).catch(() => []);
|
||||
return Array.isArray(results) ? results : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.AccessibilityCollector = AccessibilityCollector;
|
||||
56
dist/plugins/collectors/DOMSnapshotCollector.js
vendored
Normal file
56
dist/plugins/collectors/DOMSnapshotCollector.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
/**
|
||||
* DOMSnapshotCollector — writes the DOM snapshot at anomaly moment to disk.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DOMSnapshotCollector = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
class DOMSnapshotCollector {
|
||||
constructor(outputDir = './reports') {
|
||||
this.outputDir = outputDir;
|
||||
this.name = 'DOMSnapshotCollector';
|
||||
}
|
||||
async collect(anomaly, agent) {
|
||||
const state = await agent.captureState();
|
||||
const dir = path.join(this.outputDir, anomaly.id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const domPath = path.join(dir, 'dom.html');
|
||||
fs.writeFileSync(domPath, state.domSnapshot, 'utf8');
|
||||
return { domSnapshotPath: path.relative(this.outputDir, domPath) };
|
||||
}
|
||||
}
|
||||
exports.DOMSnapshotCollector = DOMSnapshotCollector;
|
||||
18
dist/plugins/collectors/NetworkCollector.js
vendored
Normal file
18
dist/plugins/collectors/NetworkCollector.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
/**
|
||||
* NetworkCollector — logs all HTTP responses from the current observation.
|
||||
* The data is already captured in the observation; this collector formats it.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NetworkCollector = void 0;
|
||||
class NetworkCollector {
|
||||
constructor() {
|
||||
this.name = 'NetworkCollector';
|
||||
}
|
||||
async collect(anomaly, _agent) {
|
||||
// HTTP responses are captured in the observation → anomaly evidence
|
||||
const httpLog = anomaly.evidence.httpLog ?? [];
|
||||
return { httpLog };
|
||||
}
|
||||
}
|
||||
exports.NetworkCollector = NetworkCollector;
|
||||
177
dist/plugins/collectors/PerformanceCollector.js
vendored
Normal file
177
dist/plugins/collectors/PerformanceCollector.js
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
"use strict";
|
||||
/**
|
||||
* PerformanceCollector — captures Navigation Timing and Core Web Vitals after each navigation.
|
||||
* Detects performance_degradation anomalies based on configurable thresholds.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PerformanceCollector = exports.DEFAULT_PERF_CONFIG = void 0;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
exports.DEFAULT_PERF_CONFIG = {
|
||||
enabled: true,
|
||||
lcpThresholdMs: 4000,
|
||||
clsThreshold: 0.25,
|
||||
inpThresholdMs: 500,
|
||||
ttfbThresholdMs: 1800,
|
||||
};
|
||||
class PerformanceCollector {
|
||||
constructor(config = {}) {
|
||||
this.metricsStore = [];
|
||||
this.config = { ...exports.DEFAULT_PERF_CONFIG, ...config };
|
||||
}
|
||||
async collect(page, stateId, sessionId, actionTrace) {
|
||||
if (!this.config.enabled) {
|
||||
const empty = {
|
||||
id: crypto.randomUUID(), sessionId, stateId, url: page.url(),
|
||||
ttfb: 0, domContentLoaded: 0, loadComplete: 0,
|
||||
lcp: null, cls: null, fid: null, inp: null,
|
||||
totalRequests: 0, failedRequests: 0, capturedAt: Date.now(),
|
||||
};
|
||||
return { metrics: empty, anomalies: [] };
|
||||
}
|
||||
// Capture Navigation Timing
|
||||
const timing = await page.evaluate(() => {
|
||||
const t = performance.timing;
|
||||
return {
|
||||
ttfb: t.responseStart - t.requestStart,
|
||||
domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
|
||||
loadComplete: t.loadEventEnd - t.navigationStart,
|
||||
};
|
||||
}).catch(() => ({ ttfb: 0, domContentLoaded: 0, loadComplete: 0 }));
|
||||
// Capture Core Web Vitals via PerformanceObserver
|
||||
const vitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const result = {
|
||||
lcp: null, cls: null, inp: null,
|
||||
};
|
||||
try {
|
||||
// Try to observe LCP
|
||||
if ('PerformanceObserver' in window) {
|
||||
try {
|
||||
const lcpObs = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
result.lcp = entries[entries.length - 1].startTime;
|
||||
}
|
||||
});
|
||||
lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });
|
||||
}
|
||||
catch { /* not supported */ }
|
||||
try {
|
||||
const clsObs = new PerformanceObserver((list) => {
|
||||
let clsScore = 0;
|
||||
for (const entry of list.getEntries()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
clsScore += entry.value ?? 0;
|
||||
}
|
||||
result.cls = clsScore;
|
||||
});
|
||||
clsObs.observe({ type: 'layout-shift', buffered: true });
|
||||
}
|
||||
catch { /* not supported */ }
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
// Resolve after short wait
|
||||
setTimeout(() => resolve(result), 500);
|
||||
});
|
||||
}).catch(() => ({ lcp: null, cls: null, inp: null }));
|
||||
const metrics = {
|
||||
id: crypto.randomUUID(),
|
||||
sessionId,
|
||||
stateId,
|
||||
url: page.url(),
|
||||
ttfb: timing.ttfb,
|
||||
domContentLoaded: timing.domContentLoaded,
|
||||
loadComplete: timing.loadComplete,
|
||||
lcp: vitals.lcp,
|
||||
cls: vitals.cls,
|
||||
fid: null,
|
||||
inp: vitals.inp,
|
||||
totalRequests: 0,
|
||||
failedRequests: 0,
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
this.metricsStore.push(metrics);
|
||||
const anomalies = this.detectAnomalies(metrics, stateId, actionTrace);
|
||||
return { metrics, anomalies };
|
||||
}
|
||||
getMetrics() {
|
||||
return this.metricsStore;
|
||||
}
|
||||
detectAnomalies(metrics, stateId, actionTrace) {
|
||||
const anomalies = [];
|
||||
const issues = [];
|
||||
let severityRank = 0; // 0=low,1=medium,2=high,3=critical
|
||||
if (metrics.lcp !== null && metrics.lcp > this.config.lcpThresholdMs) {
|
||||
issues.push(`LCP: ${metrics.lcp}ms (threshold: ${this.config.lcpThresholdMs}ms)`);
|
||||
if (severityRank < 2)
|
||||
severityRank = 2; // high
|
||||
}
|
||||
if (metrics.cls !== null && metrics.cls > this.config.clsThreshold) {
|
||||
issues.push(`CLS: ${metrics.cls.toFixed(3)} (threshold: ${this.config.clsThreshold})`);
|
||||
if (severityRank < 1)
|
||||
severityRank = 1; // medium
|
||||
}
|
||||
if (metrics.inp !== null && metrics.inp > this.config.inpThresholdMs) {
|
||||
issues.push(`INP: ${metrics.inp}ms (threshold: ${this.config.inpThresholdMs}ms)`);
|
||||
if (severityRank < 2)
|
||||
severityRank = 2; // high
|
||||
}
|
||||
if (metrics.ttfb > this.config.ttfbThresholdMs) {
|
||||
issues.push(`TTFB: ${metrics.ttfb}ms (threshold: ${this.config.ttfbThresholdMs}ms)`);
|
||||
if (severityRank < 1)
|
||||
severityRank = 1; // medium
|
||||
}
|
||||
const RANK_TO_SEVERITY = ['low', 'medium', 'high', 'critical'];
|
||||
const maxSeverity = RANK_TO_SEVERITY[severityRank] ?? 'low';
|
||||
if (issues.length === 0)
|
||||
return anomalies;
|
||||
anomalies.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'performance_degradation',
|
||||
severity: maxSeverity,
|
||||
observationId: stateId,
|
||||
actionTrace,
|
||||
description: `Performance degradation at ${metrics.url}: ${issues[0]}`,
|
||||
evidence: {
|
||||
rawErrors: issues,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return anomalies;
|
||||
}
|
||||
}
|
||||
exports.PerformanceCollector = PerformanceCollector;
|
||||
63
dist/plugins/collectors/ScreenshotCollector.js
vendored
Normal file
63
dist/plugins/collectors/ScreenshotCollector.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ScreenshotCollector — captures a PNG screenshot at anomaly moment.
|
||||
* Requires the agent to be a PlaywrightAgent (duck-typing check).
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ScreenshotCollector = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
function isPlaywrightAgent(agent) {
|
||||
return typeof agent.getPage === 'function';
|
||||
}
|
||||
class ScreenshotCollector {
|
||||
constructor(outputDir = './reports') {
|
||||
this.outputDir = outputDir;
|
||||
this.name = 'ScreenshotCollector';
|
||||
}
|
||||
async collect(anomaly, agent) {
|
||||
if (!isPlaywrightAgent(agent)) {
|
||||
return {};
|
||||
}
|
||||
const page = agent.getPage();
|
||||
const dir = path.join(this.outputDir, anomaly.id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const screenshotPath = path.join(dir, 'screenshot.png');
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return { screenshotPath: path.relative(this.outputDir, screenshotPath) };
|
||||
}
|
||||
}
|
||||
exports.ScreenshotCollector = ScreenshotCollector;
|
||||
155
dist/plugins/collectors/VisualRegressionCollector.js
vendored
Normal file
155
dist/plugins/collectors/VisualRegressionCollector.js
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
"use strict";
|
||||
/**
|
||||
* VisualRegressionCollector — captures screenshots and compares against baselines.
|
||||
* Uses pixelmatch for pixel-level comparison and sharp for image normalization.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VisualRegressionCollector = exports.DEFAULT_VISUAL_CONFIG = void 0;
|
||||
exports.compareScreenshots = compareScreenshots;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
exports.DEFAULT_VISUAL_CONFIG = {
|
||||
enabled: true,
|
||||
threshold: 0.001,
|
||||
screenshotFullPage: false,
|
||||
ignoreSelectors: [],
|
||||
};
|
||||
async function compareScreenshots(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
||||
// Dynamic imports to avoid loading heavy deps at startup
|
||||
const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
|
||||
const pixelmatch = (await Promise.resolve().then(() => __importStar(require('pixelmatch')))).default;
|
||||
const [baselineRaw, currentRaw] = await Promise.all([
|
||||
sharp(baselinePath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
|
||||
sharp(currentPath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
|
||||
]);
|
||||
const { width, height } = baselineRaw.info;
|
||||
const diffBuffer = Buffer.alloc(width * height * 4);
|
||||
const diffPixels = pixelmatch(baselineRaw.data, currentRaw.data, diffBuffer, width, height, { threshold });
|
||||
const totalPixels = width * height;
|
||||
const diffPercent = totalPixels > 0 ? diffPixels / totalPixels : 0;
|
||||
// Write diff image
|
||||
await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
|
||||
.png()
|
||||
.toFile(diffOutputPath);
|
||||
return { diffPixels, diffPercent, hasDiff: diffPixels > 0 };
|
||||
}
|
||||
class VisualRegressionCollector {
|
||||
constructor(outputDir, repo, config = {}) {
|
||||
this.outputDir = outputDir;
|
||||
this.repo = repo;
|
||||
this.config = { ...exports.DEFAULT_VISUAL_CONFIG, ...config };
|
||||
}
|
||||
/**
|
||||
* Process a screenshot for visual regression.
|
||||
* Returns an anomaly if a regression is detected, otherwise null.
|
||||
*/
|
||||
async processScreenshot(screenshotPath, state, sessionId, actionTrace) {
|
||||
if (!this.config.enabled)
|
||||
return null;
|
||||
const comparisonId = crypto.randomUUID();
|
||||
const baseline = this.repo.findBaselineByStateId(state.id);
|
||||
if (!baseline) {
|
||||
// No baseline: create a new_state comparison record
|
||||
this.repo.createComparison({
|
||||
id: comparisonId,
|
||||
sessionId,
|
||||
stateId: state.id,
|
||||
currentScreenshotPath: screenshotPath,
|
||||
status: 'new_state',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
// Compare against baseline
|
||||
const diffDir = path.join(this.outputDir, 'visual', comparisonId);
|
||||
if (!fs.existsSync(diffDir)) {
|
||||
fs.mkdirSync(diffDir, { recursive: true });
|
||||
}
|
||||
const diffPath = path.join(diffDir, 'diff.png');
|
||||
let diffPixels = 0;
|
||||
let diffPercent = 0;
|
||||
try {
|
||||
const result = await compareScreenshots(baseline.screenshot_path, screenshotPath, diffPath, this.config.threshold);
|
||||
diffPixels = result.diffPixels;
|
||||
diffPercent = result.diffPercent;
|
||||
}
|
||||
catch {
|
||||
// If comparison fails (e.g. image format issues), skip
|
||||
return null;
|
||||
}
|
||||
const thresholdPct = this.config.threshold;
|
||||
const status = diffPercent > thresholdPct ? 'failed' : 'passed';
|
||||
this.repo.createComparison({
|
||||
id: comparisonId,
|
||||
sessionId,
|
||||
stateId: state.id,
|
||||
baselineId: baseline.id,
|
||||
currentScreenshotPath: screenshotPath,
|
||||
diffScreenshotPath: status === 'failed' ? diffPath : undefined,
|
||||
diffPixels,
|
||||
diffPercent,
|
||||
status,
|
||||
});
|
||||
if (status !== 'failed')
|
||||
return null;
|
||||
// Determine severity from diff percent
|
||||
const pct = diffPercent * 100;
|
||||
let severity;
|
||||
if (pct > 15)
|
||||
severity = 'critical';
|
||||
else if (pct > 5)
|
||||
severity = 'high';
|
||||
else if (pct > 1)
|
||||
severity = 'medium';
|
||||
else
|
||||
severity = 'low';
|
||||
const anomaly = {
|
||||
id: crypto.randomUUID(),
|
||||
type: 'visual_regression',
|
||||
severity,
|
||||
observationId: state.id,
|
||||
actionTrace,
|
||||
description: `Visual regression detected: ${(pct).toFixed(2)}% of pixels changed`,
|
||||
evidence: {
|
||||
screenshotPath: diffPath,
|
||||
rawErrors: [`Diff: ${diffPixels} pixels (${(pct).toFixed(2)}%)`],
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return anomaly;
|
||||
}
|
||||
}
|
||||
exports.VisualRegressionCollector = VisualRegressionCollector;
|
||||
97
dist/plugins/exporters/JSONExporter.js
vendored
Normal file
97
dist/plugins/exporters/JSONExporter.js
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
"use strict";
|
||||
/**
|
||||
* JSONExporter — produces a structured JSON report for AI debugging workflows.
|
||||
* Output: reports/{anomaly-id}/report.json
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.JSONExporter = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
class JSONExporter {
|
||||
constructor(targetUrl = '', abeVersion = '0.1.0') {
|
||||
this.targetUrl = targetUrl;
|
||||
this.abeVersion = abeVersion;
|
||||
this.format = 'json';
|
||||
}
|
||||
async export(anomaly, outputDir) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const report = {
|
||||
version: '1.0',
|
||||
generated_at: new Date(anomaly.timestamp).toISOString(),
|
||||
environment: {
|
||||
target_url: this.targetUrl,
|
||||
abe_version: this.abeVersion,
|
||||
os: os.platform(),
|
||||
node_version: process.version,
|
||||
},
|
||||
anomaly: {
|
||||
id: anomaly.id,
|
||||
type: anomaly.type,
|
||||
severity: anomaly.severity,
|
||||
description: anomaly.description,
|
||||
timestamp: anomaly.timestamp,
|
||||
},
|
||||
reproduction: {
|
||||
seed: anomaly.actionTrace[0]?.seed ?? null,
|
||||
steps: anomaly.actionTrace.map((action, index) => ({
|
||||
step: index + 1,
|
||||
action_type: action.type,
|
||||
selector: action.selector,
|
||||
value: action.value,
|
||||
url: action.url,
|
||||
timestamp: action.timestamp,
|
||||
})),
|
||||
},
|
||||
evidence: {
|
||||
screenshot: anomaly.evidence.screenshotPath ?? null,
|
||||
dom_snapshot: anomaly.evidence.domSnapshotPath ?? null,
|
||||
http_log: (anomaly.evidence.httpLog ?? []).map((r) => ({
|
||||
url: r.url,
|
||||
method: r.method,
|
||||
status: r.status,
|
||||
duration_ms: r.durationMs,
|
||||
})),
|
||||
console_errors: anomaly.evidence.rawErrors?.filter((e) => e.startsWith('console:')) ?? [],
|
||||
js_exceptions: anomaly.evidence.rawErrors?.filter((e) => !e.startsWith('console:')) ?? [],
|
||||
},
|
||||
};
|
||||
const filePath = path.join(outputDir, 'report.json');
|
||||
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
exports.JSONExporter = JSONExporter;
|
||||
113
dist/plugins/exporters/MarkdownExporter.js
vendored
Normal file
113
dist/plugins/exporters/MarkdownExporter.js
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
"use strict";
|
||||
/**
|
||||
* MarkdownExporter — produces a human-readable bug report.
|
||||
* Output: reports/{anomaly-id}/report.md
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MarkdownExporter = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
class MarkdownExporter {
|
||||
constructor() {
|
||||
this.format = 'markdown';
|
||||
}
|
||||
async export(anomaly, outputDir) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const date = new Date(anomaly.timestamp).toISOString().split('T')[0];
|
||||
const seed = anomaly.actionTrace[0]?.seed ?? 'N/A';
|
||||
const replayCmd = `npm run replay -- --report ${outputDir}/report.json`;
|
||||
const steps = anomaly.actionTrace
|
||||
.map((action, i) => {
|
||||
switch (action.type) {
|
||||
case 'navigate':
|
||||
return `${i + 1}. Navigate to \`${action.url}\``;
|
||||
case 'click':
|
||||
return `${i + 1}. Click element \`${action.selector}\``;
|
||||
case 'fill':
|
||||
return `${i + 1}. Fill \`${action.selector}\` with \`${JSON.stringify(action.value ?? '')}\``;
|
||||
case 'select':
|
||||
return `${i + 1}. Select \`${action.value}\` in \`${action.selector}\``;
|
||||
case 'submit':
|
||||
return `${i + 1}. Submit form \`${action.selector}\``;
|
||||
default:
|
||||
return `${i + 1}. ${action.type}`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
const httpTable = (anomaly.evidence.httpLog ?? []).length > 0
|
||||
? [
|
||||
'| Method | URL | Status | Duration |',
|
||||
'|--------|-----|--------|----------|',
|
||||
...(anomaly.evidence.httpLog ?? []).map((r) => `| ${r.method} | ${r.url} | ${r.status} | ${r.durationMs}ms |`),
|
||||
].join('\n')
|
||||
: '_No HTTP log available._';
|
||||
const rawErrors = (anomaly.evidence.rawErrors ?? []).length > 0
|
||||
? '```\n' + anomaly.evidence.rawErrors.join('\n') + '\n```'
|
||||
: '_No raw errors recorded._';
|
||||
const md = `# Bug Report — ${anomaly.type} — ${date}
|
||||
|
||||
## Summary
|
||||
${anomaly.description}
|
||||
|
||||
## Severity
|
||||
**${anomaly.severity}** — detected by ABE heuristic rule \`${anomaly.type}\`
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
${steps.length > 0 ? steps : '_No steps recorded._'}
|
||||
|
||||
**Seed used**: \`${seed}\`
|
||||
**Replay command**: \`${replayCmd}\`
|
||||
|
||||
## Observed Behavior
|
||||
${anomaly.description}
|
||||
|
||||
## Evidence
|
||||
- Screenshot: \`${anomaly.evidence.screenshotPath ?? 'N/A'}\`
|
||||
- DOM Snapshot: \`${anomaly.evidence.domSnapshotPath ?? 'N/A'}\`
|
||||
- HTTP Log:
|
||||
|
||||
${httpTable}
|
||||
|
||||
## Raw Errors
|
||||
${rawErrors}
|
||||
`;
|
||||
const filePath = path.join(outputDir, 'report.md');
|
||||
fs.writeFileSync(filePath, md, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
exports.MarkdownExporter = MarkdownExporter;
|
||||
139
dist/plugins/fuzzers/FuzzingEngine.js
vendored
Normal file
139
dist/plugins/fuzzers/FuzzingEngine.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
"use strict";
|
||||
/**
|
||||
* FuzzingEngine — orchestrates fuzzing strategies for form inputs.
|
||||
* Implements IFuzzingPlugin so ExplorationEngine doesn't need to import it directly.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FuzzingEngine = void 0;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const InputTypeDetector_1 = require("./InputTypeDetector");
|
||||
const EmptyValueStrategy_1 = require("./strategies/EmptyValueStrategy");
|
||||
const OversizedStringStrategy_1 = require("./strategies/OversizedStringStrategy");
|
||||
const SpecialCharsStrategy_1 = require("./strategies/SpecialCharsStrategy");
|
||||
const TypeMismatchStrategy_1 = require("./strategies/TypeMismatchStrategy");
|
||||
const BoundaryValueStrategy_1 = require("./strategies/BoundaryValueStrategy");
|
||||
/** Regex to match basic input elements in an HTML string */
|
||||
const INPUT_RE = /<(input|textarea|select)[^>]*>/gi;
|
||||
const ATTR_RE = (name) => new RegExp(`${name}="([^"]*)"`, 'i');
|
||||
function extractFields(domSnapshot) {
|
||||
const fields = [];
|
||||
let match;
|
||||
while ((match = INPUT_RE.exec(domSnapshot)) !== null) {
|
||||
const tag = match[0] ?? '';
|
||||
const tagName = match[1] ?? 'input';
|
||||
const idMatch = ATTR_RE('id').exec(tag);
|
||||
const nameMatch = ATTR_RE('name').exec(tag);
|
||||
const typeMatch = ATTR_RE('type').exec(tag);
|
||||
const placeholderMatch = ATTR_RE('placeholder').exec(tag);
|
||||
const ariaMatch = ATTR_RE('aria-label').exec(tag);
|
||||
const selector = idMatch?.[1]
|
||||
? `#${idMatch[1]}`
|
||||
: nameMatch?.[1]
|
||||
? `[name="${nameMatch[1]}"]`
|
||||
: tagName;
|
||||
fields.push({
|
||||
selector,
|
||||
tagName,
|
||||
inputType: typeMatch?.[1],
|
||||
name: nameMatch?.[1],
|
||||
placeholder: placeholderMatch?.[1],
|
||||
ariaLabel: ariaMatch?.[1],
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
class FuzzingEngine {
|
||||
constructor(config) {
|
||||
this.intensity = config.intensity;
|
||||
this.seed = config.seed;
|
||||
}
|
||||
/** IFuzzingPlugin implementation — parses fields from DOM snapshot */
|
||||
generateFuzzActions(domSnapshot, state) {
|
||||
const fields = extractFields(domSnapshot);
|
||||
return this.generateFuzzActionsForFields(fields, state);
|
||||
}
|
||||
/** Generate fuzz actions from explicit field descriptors */
|
||||
generateFuzzActionsForFields(fields, state) {
|
||||
const actions = [];
|
||||
const now = Date.now();
|
||||
const strategies = this.selectStrategies();
|
||||
for (const field of fields) {
|
||||
const detectedType = (0, InputTypeDetector_1.detectInputType)({
|
||||
tagName: field.tagName,
|
||||
inputType: field.inputType,
|
||||
name: field.name,
|
||||
placeholder: field.placeholder,
|
||||
ariaLabel: field.ariaLabel,
|
||||
});
|
||||
for (const strategy of strategies) {
|
||||
const values = this.getValuesFromStrategy(strategy, detectedType);
|
||||
for (const value of values) {
|
||||
actions.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'fill',
|
||||
selector: field.selector,
|
||||
value,
|
||||
timestamp: now,
|
||||
seed: this.seed,
|
||||
stateId: state.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
selectStrategies() {
|
||||
const empty = new EmptyValueStrategy_1.EmptyValueStrategy();
|
||||
const typeMismatch = new TypeMismatchStrategy_1.TypeMismatchStrategy();
|
||||
const oversized = new OversizedStringStrategy_1.OversizedStringStrategy(this.intensity);
|
||||
const boundary = new BoundaryValueStrategy_1.BoundaryValueStrategy();
|
||||
const special = new SpecialCharsStrategy_1.SpecialCharsStrategy();
|
||||
switch (this.intensity) {
|
||||
case 'low':
|
||||
return [empty, typeMismatch];
|
||||
case 'medium':
|
||||
return [empty, typeMismatch, oversized, boundary];
|
||||
case 'high':
|
||||
return [empty, typeMismatch, oversized, boundary, special];
|
||||
}
|
||||
}
|
||||
getValuesFromStrategy(strategy, type) {
|
||||
if (!strategy.appliesTo(type))
|
||||
return [];
|
||||
return strategy.values(type);
|
||||
}
|
||||
}
|
||||
exports.FuzzingEngine = FuzzingEngine;
|
||||
52
dist/plugins/fuzzers/InputTypeDetector.js
vendored
Normal file
52
dist/plugins/fuzzers/InputTypeDetector.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
/**
|
||||
* InputTypeDetector — detects field type from DOM attributes.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.detectInputType = detectInputType;
|
||||
/** Detect type from input[type], name, placeholder, aria-label */
|
||||
function detectInputType(attrs) {
|
||||
const tag = (attrs.tagName ?? '').toLowerCase();
|
||||
if (tag === 'textarea')
|
||||
return 'textarea';
|
||||
if (tag === 'select')
|
||||
return 'select';
|
||||
const inputType = (attrs.inputType ?? '').toLowerCase();
|
||||
if (inputType === 'email')
|
||||
return 'email';
|
||||
if (inputType === 'password')
|
||||
return 'password';
|
||||
if (inputType === 'number')
|
||||
return 'number';
|
||||
if (inputType === 'date')
|
||||
return 'date';
|
||||
if (inputType === 'tel')
|
||||
return 'phone';
|
||||
if (inputType === 'url')
|
||||
return 'url';
|
||||
if (inputType === 'search')
|
||||
return 'search';
|
||||
if (inputType === 'file')
|
||||
return 'file';
|
||||
// Infer from name/placeholder/aria-label
|
||||
const hints = [
|
||||
(attrs.name ?? '').toLowerCase(),
|
||||
(attrs.placeholder ?? '').toLowerCase(),
|
||||
(attrs.ariaLabel ?? '').toLowerCase(),
|
||||
].join(' ');
|
||||
if (/email/.test(hints))
|
||||
return 'email';
|
||||
if (/password|pass/.test(hints))
|
||||
return 'password';
|
||||
if (/phone|tel|mobile/.test(hints))
|
||||
return 'phone';
|
||||
if (/date|birth|dob/.test(hints))
|
||||
return 'date';
|
||||
if (/number|qty|quantity|age/.test(hints))
|
||||
return 'number';
|
||||
if (/search/.test(hints))
|
||||
return 'search';
|
||||
if (/url|website|link/.test(hints))
|
||||
return 'url';
|
||||
return 'text';
|
||||
}
|
||||
26
dist/plugins/fuzzers/strategies/BoundaryValueStrategy.js
vendored
Normal file
26
dist/plugins/fuzzers/strategies/BoundaryValueStrategy.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
/**
|
||||
* BoundaryValueStrategy — tests values at the edges of expected ranges.
|
||||
* Applies to: number, date.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BoundaryValueStrategy = void 0;
|
||||
class BoundaryValueStrategy {
|
||||
constructor() {
|
||||
this.name = 'BoundaryValueStrategy';
|
||||
}
|
||||
appliesTo(type) {
|
||||
return type === 'number' || type === 'date';
|
||||
}
|
||||
values(type) {
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return ['0', '-1', '2147483647', '2147483648', '-2147483648'];
|
||||
case 'date':
|
||||
return ['1900-01-01', '2099-12-31', '1970-01-01'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.BoundaryValueStrategy = BoundaryValueStrategy;
|
||||
19
dist/plugins/fuzzers/strategies/EmptyValueStrategy.js
vendored
Normal file
19
dist/plugins/fuzzers/strategies/EmptyValueStrategy.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
/**
|
||||
* EmptyValueStrategy — submits empty/whitespace values to catch missing server-side validation.
|
||||
* Applies to: all input types.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.EmptyValueStrategy = void 0;
|
||||
class EmptyValueStrategy {
|
||||
constructor() {
|
||||
this.name = 'EmptyValueStrategy';
|
||||
}
|
||||
appliesTo(_type) {
|
||||
return true;
|
||||
}
|
||||
values() {
|
||||
return ['', ' ', '\t'];
|
||||
}
|
||||
}
|
||||
exports.EmptyValueStrategy = EmptyValueStrategy;
|
||||
28
dist/plugins/fuzzers/strategies/OversizedStringStrategy.js
vendored
Normal file
28
dist/plugins/fuzzers/strategies/OversizedStringStrategy.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
/**
|
||||
* OversizedStringStrategy — submits strings far beyond expected length.
|
||||
* Applies to: text, email, password, textarea.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OversizedStringStrategy = void 0;
|
||||
const APPLICABLE_TYPES = ['text', 'email', 'password', 'textarea'];
|
||||
class OversizedStringStrategy {
|
||||
constructor(intensity) {
|
||||
this.intensity = intensity;
|
||||
this.name = 'OversizedStringStrategy';
|
||||
}
|
||||
appliesTo(type) {
|
||||
return APPLICABLE_TYPES.includes(type);
|
||||
}
|
||||
values() {
|
||||
switch (this.intensity) {
|
||||
case 'low':
|
||||
return ['A'.repeat(256)];
|
||||
case 'medium':
|
||||
return ['A'.repeat(1024)];
|
||||
case 'high':
|
||||
return ['A'.repeat(10000) + '日本語テスト𠮷野家'];
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.OversizedStringStrategy = OversizedStringStrategy;
|
||||
26
dist/plugins/fuzzers/strategies/SpecialCharsStrategy.js
vendored
Normal file
26
dist/plugins/fuzzers/strategies/SpecialCharsStrategy.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
/**
|
||||
* SpecialCharsStrategy — injects characters that break SQL, HTML, and shell contexts.
|
||||
* Applies to: text, email, search, textarea.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SpecialCharsStrategy = void 0;
|
||||
const APPLICABLE_TYPES = ['text', 'email', 'search', 'textarea'];
|
||||
class SpecialCharsStrategy {
|
||||
constructor() {
|
||||
this.name = 'SpecialCharsStrategy';
|
||||
}
|
||||
appliesTo(type) {
|
||||
return APPLICABLE_TYPES.includes(type);
|
||||
}
|
||||
values() {
|
||||
return [
|
||||
"' OR 1=1 --",
|
||||
'<script>alert(1)</script>',
|
||||
'../../etc/passwd',
|
||||
'${7*7}',
|
||||
'\x00\x01\x02',
|
||||
];
|
||||
}
|
||||
}
|
||||
exports.SpecialCharsStrategy = SpecialCharsStrategy;
|
||||
31
dist/plugins/fuzzers/strategies/TypeMismatchStrategy.js
vendored
Normal file
31
dist/plugins/fuzzers/strategies/TypeMismatchStrategy.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
/**
|
||||
* TypeMismatchStrategy — submits wrong data types for the detected field type.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TypeMismatchStrategy = void 0;
|
||||
class TypeMismatchStrategy {
|
||||
constructor() {
|
||||
this.name = 'TypeMismatchStrategy';
|
||||
}
|
||||
appliesTo(type) {
|
||||
return ['email', 'number', 'date', 'url', 'phone'].includes(type);
|
||||
}
|
||||
values(type) {
|
||||
switch (type) {
|
||||
case 'email':
|
||||
return ['not-an-email', '12345', '@@@'];
|
||||
case 'number':
|
||||
return ['abc', '-999999', '9.9.9', 'NaN'];
|
||||
case 'date':
|
||||
return ['yesterday', '32/13/2025', '0000-00-00'];
|
||||
case 'url':
|
||||
return ['javascript:alert(1)', 'not a url'];
|
||||
case 'phone':
|
||||
return ['000', '++++', 'abcdefghij'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TypeMismatchStrategy = TypeMismatchStrategy;
|
||||
7
dist/plugins/interfaces.js
vendored
Normal file
7
dist/plugins/interfaces.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Plugin interfaces re-exported from core for plugin implementations to use.
|
||||
* All interface definitions live in src/core/interfaces.ts so that core
|
||||
* code can depend on them without creating a circular dependency.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
59
dist/plugins/reproducers/PlaywrightReproducer.js
vendored
Normal file
59
dist/plugins/reproducers/PlaywrightReproducer.js
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
/**
|
||||
* PlaywrightReproducer — serializes an action trace and generates a
|
||||
* deterministic Playwright script for replay.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PlaywrightReproducer = void 0;
|
||||
class PlaywrightReproducer {
|
||||
serialize(trace) {
|
||||
return JSON.stringify(trace, null, 2);
|
||||
}
|
||||
deserialize(raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('PlaywrightReproducer.deserialize: expected a JSON array');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
generateScript(trace) {
|
||||
const lines = [
|
||||
'// Auto-generated replay script by ABE (Autonomous Bug Explorer)',
|
||||
`// Generated at: ${new Date().toISOString()}`,
|
||||
`// Steps: ${trace.length}`,
|
||||
'',
|
||||
"const { chromium } = require('playwright');",
|
||||
'',
|
||||
'(async () => {',
|
||||
' const browser = await chromium.launch({ headless: true });',
|
||||
' const context = await browser.newContext();',
|
||||
' const page = await context.newPage();',
|
||||
'',
|
||||
];
|
||||
for (let i = 0; i < trace.length; i++) {
|
||||
const action = trace[i];
|
||||
lines.push(` // Step ${i + 1}: ${action.type} (seed=${action.seed})`);
|
||||
switch (action.type) {
|
||||
case 'navigate':
|
||||
lines.push(` await page.goto(${JSON.stringify(action.url)});`);
|
||||
break;
|
||||
case 'click':
|
||||
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().click();`);
|
||||
break;
|
||||
case 'fill':
|
||||
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().fill(${JSON.stringify(action.value ?? '')});`);
|
||||
break;
|
||||
case 'select':
|
||||
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().selectOption(${JSON.stringify(action.value ?? '')});`);
|
||||
break;
|
||||
case 'submit':
|
||||
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().dispatchEvent('submit');`);
|
||||
break;
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
lines.push(" console.log('Replay complete');", ' await browser.close();', '})();');
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
exports.PlaywrightReproducer = PlaywrightReproducer;
|
||||
Reference in New Issue
Block a user