ghost-qa 0.0.0-stage → 1.3.0
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 +273 -3
- package/SKILL.md +105 -0
- package/dist/bin/ghost-qa.d.ts +35 -0
- package/dist/bin/ghost-qa.d.ts.map +1 -0
- package/dist/bin/ghost-qa.js +406 -0
- package/dist/bin/ghost-qa.js.map +1 -0
- package/dist/src/engine/audit-runner.d.ts +10 -0
- package/dist/src/engine/audit-runner.d.ts.map +1 -0
- package/dist/src/engine/audit-runner.js +138 -0
- package/dist/src/engine/audit-runner.js.map +1 -0
- package/dist/src/engine/browser-session.d.ts +37 -0
- package/dist/src/engine/browser-session.d.ts.map +1 -0
- package/dist/src/engine/browser-session.js +114 -0
- package/dist/src/engine/browser-session.js.map +1 -0
- package/dist/src/engine/interaction-runner.d.ts +41 -0
- package/dist/src/engine/interaction-runner.d.ts.map +1 -0
- package/dist/src/engine/interaction-runner.js +274 -0
- package/dist/src/engine/interaction-runner.js.map +1 -0
- package/dist/src/engine/route-crawler.d.ts +31 -0
- package/dist/src/engine/route-crawler.d.ts.map +1 -0
- package/dist/src/engine/route-crawler.js +110 -0
- package/dist/src/engine/route-crawler.js.map +1 -0
- package/dist/src/engine/server-lifecycle.d.ts +41 -0
- package/dist/src/engine/server-lifecycle.d.ts.map +1 -0
- package/dist/src/engine/server-lifecycle.js +154 -0
- package/dist/src/engine/server-lifecycle.js.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/server.d.ts +111 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +267 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/reporter/agent-diagnostic.d.ts +17 -0
- package/dist/src/reporter/agent-diagnostic.d.ts.map +1 -0
- package/dist/src/reporter/agent-diagnostic.js +80 -0
- package/dist/src/reporter/agent-diagnostic.js.map +1 -0
- package/dist/src/reporter/html-report.d.ts +9 -0
- package/dist/src/reporter/html-report.d.ts.map +1 -0
- package/dist/src/reporter/html-report.js +142 -0
- package/dist/src/reporter/html-report.js.map +1 -0
- package/dist/src/reporter/terminal-summary.d.ts +5 -0
- package/dist/src/reporter/terminal-summary.d.ts.map +1 -0
- package/dist/src/reporter/terminal-summary.js +71 -0
- package/dist/src/reporter/terminal-summary.js.map +1 -0
- package/dist/src/traps/a11y-trap.d.ts +27 -0
- package/dist/src/traps/a11y-trap.d.ts.map +1 -0
- package/dist/src/traps/a11y-trap.js +173 -0
- package/dist/src/traps/a11y-trap.js.map +1 -0
- package/dist/src/traps/console-trap.d.ts +28 -0
- package/dist/src/traps/console-trap.d.ts.map +1 -0
- package/dist/src/traps/console-trap.js +151 -0
- package/dist/src/traps/console-trap.js.map +1 -0
- package/dist/src/traps/layout-trap.d.ts +31 -0
- package/dist/src/traps/layout-trap.d.ts.map +1 -0
- package/dist/src/traps/layout-trap.js +150 -0
- package/dist/src/traps/layout-trap.js.map +1 -0
- package/dist/src/traps/network-trap.d.ts +24 -0
- package/dist/src/traps/network-trap.d.ts.map +1 -0
- package/dist/src/traps/network-trap.js +132 -0
- package/dist/src/traps/network-trap.js.map +1 -0
- package/dist/src/traps/perf-trap.d.ts +42 -0
- package/dist/src/traps/perf-trap.d.ts.map +1 -0
- package/dist/src/traps/perf-trap.js +196 -0
- package/dist/src/traps/perf-trap.js.map +1 -0
- package/dist/src/types.d.ts +95 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +80 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +91 -6
- package/scripts/prepare-build.mjs +40 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-efficient fix guidance per category. The agent reading the diagnostic
|
|
3
|
+
* should be able to act without re-discovering first principles each run.
|
|
4
|
+
*/
|
|
5
|
+
export const FIX_PLAYBOOK = {
|
|
6
|
+
console: 'Open the route, reproduce in a browser, and fix the throwing code path. For hydration mismatches, ensure server and client render identical markup (no Date/random/window at render time, no invalid HTML nesting like <p> inside <p>).',
|
|
7
|
+
network: 'Check the failing endpoint/asset: verify the API route exists, returns 2xx, sends CORS headers for the page origin, and that static asset paths match files actually emitted to the public/build output.',
|
|
8
|
+
layout: 'Inspect the flagged elements: constrain widths (max-width: 100%, min-width: 0 on flex/grid children), avoid 100vw with scrollbars, restore collapsed interactive elements to a visible size, and point <img> at assets that exist.',
|
|
9
|
+
accessibility: 'Give every interactive element an accessible name (visible text or aria-label), add alt text to images (alt="" only when purely decorative), associate every form input with a real <label> or aria-label, and set a per-route <title> plus <html lang>.',
|
|
10
|
+
performance: 'Bring the page inside budget: defer/async non-critical scripts, compress and lazy-load oversized assets, code-split large bundles, break up long main-thread tasks, avoid reading layout properties in loops after DOM writes, and prune excessive DOM nodes.',
|
|
11
|
+
interaction: 'Reproduce the click/fill manually: check for overlays intercepting pointer events, disabled state that never clears, handlers throwing on click, and elements re-rendering between hover and click.',
|
|
12
|
+
navigation: 'Verify the route exists, the dev server is running on the target origin, and redirects resolve to a 2xx page instead of looping or 404ing.',
|
|
13
|
+
};
|
|
14
|
+
function groupBy(items, key) {
|
|
15
|
+
const groups = new Map();
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
const k = key(item);
|
|
18
|
+
const bucket = groups.get(k) ?? [];
|
|
19
|
+
bucket.push(item);
|
|
20
|
+
groups.set(k, bucket);
|
|
21
|
+
}
|
|
22
|
+
return groups;
|
|
23
|
+
}
|
|
24
|
+
function issueLine(issue) {
|
|
25
|
+
const details = issue.details
|
|
26
|
+
? ` (${Object.entries(issue.details)
|
|
27
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
28
|
+
.join(', ')})`
|
|
29
|
+
: '';
|
|
30
|
+
return `- [#${issue.id}] (${issue.severity}/${issue.viewport} ${issue.route}) ${issue.message}${details}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The Self-Healing Bug Report: deliberately compact Markdown an agent can
|
|
34
|
+
* paste into its own context — verdict first, issues grouped by route, one
|
|
35
|
+
* fix playbook line per category, then the re-verify command.
|
|
36
|
+
*/
|
|
37
|
+
export function formatAgentDiagnostic(report) {
|
|
38
|
+
const { summary } = report;
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push('# 👻 ghost-qa — Self-Healing Bug Report');
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`**Verdict:** ${report.passed ? '✅ PASS — browser verification clean' : '❌ FAIL — do NOT report task complete'}`);
|
|
43
|
+
lines.push(`**Target:** ${report.targetUrl}`);
|
|
44
|
+
lines.push(`**Coverage:** ${summary.routesCrawled} routes · ${summary.interactionsTested} interactions passed · ${summary.interactionsFailed} failed · ${summary.interactionsSkipped} skipped (safety)`);
|
|
45
|
+
lines.push(`**Issues:** ${summary.totalIssues} (${summary.bySeverity.critical} critical, ${summary.bySeverity.major} major, ${summary.bySeverity.minor} minor)`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
if (report.issues.length === 0) {
|
|
48
|
+
lines.push('No console, network, layout, accessibility, performance or interaction failures were trapped on desktop (1440×900) or mobile (390×844).');
|
|
49
|
+
lines.push('You may report the task complete.');
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
lines.push('## Bugs to fix (grouped by route)');
|
|
53
|
+
const byRoute = groupBy(report.issues, (i) => i.route);
|
|
54
|
+
for (const [route, issues] of [...byRoute.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push(`### ${route}`);
|
|
57
|
+
for (const issue of issues)
|
|
58
|
+
lines.push(issueLine(issue));
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push('## Fix playbook');
|
|
62
|
+
const categories = [...new Set(report.issues.map((i) => i.category))];
|
|
63
|
+
for (const category of categories) {
|
|
64
|
+
lines.push(`- **${category}:** ${FIX_PLAYBOOK[category]}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('## Self-healing loop');
|
|
68
|
+
lines.push('1. Fix each bug above in source (reference issues by `#id`).');
|
|
69
|
+
lines.push(`2. Re-run: \`npx github:avivgerz-hue/ghost-qa test ${report.targetUrl}\``);
|
|
70
|
+
lines.push('3. Repeat until ghost-qa prints PASS. Only then report the task complete to the user.');
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
/** Structured JSON for CI gates and programmatic agent tooling. */
|
|
74
|
+
export function formatAgentDiagnosticJson(report) {
|
|
75
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
76
|
+
}
|
|
77
|
+
export function formatDiagnostic(report, format) {
|
|
78
|
+
return format === 'json' ? formatAgentDiagnosticJson(report) : formatAgentDiagnostic(report);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=agent-diagnostic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-diagnostic.js","sourceRoot":"","sources":["../../../src/reporter/agent-diagnostic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GhostQAReport } from '../types.js';
|
|
2
|
+
export declare function escapeHtml(value: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* A sleek, self-contained (zero external assets) dark-mode HTML dashboard
|
|
5
|
+
* summarising one ghost-qa run — routes, interactions and every trap finding
|
|
6
|
+
* across desktop and mobile. Safe to attach to CI artifacts or PRs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatHtmlReport(report: GhostQAReport): string;
|
|
9
|
+
//# sourceMappingURL=html-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-report.d.ts","sourceRoot":"","sources":["../../../src/reporter/html-report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA0B,MAAM,aAAa,CAAC;AAEzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AA8DD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAiF9D"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export function escapeHtml(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
}
|
|
9
|
+
const SEVERITY_COLOR = {
|
|
10
|
+
critical: '#f87171',
|
|
11
|
+
major: '#fbbf24',
|
|
12
|
+
minor: '#94a3b8',
|
|
13
|
+
};
|
|
14
|
+
function severityBadge(issue) {
|
|
15
|
+
const color = SEVERITY_COLOR[issue.severity];
|
|
16
|
+
return `<span class="badge" style="--c:${color}">${escapeHtml(issue.severity.toUpperCase())}</span>`;
|
|
17
|
+
}
|
|
18
|
+
function issueRow(issue) {
|
|
19
|
+
const details = issue.details
|
|
20
|
+
? `<div class="details">${escapeHtml(Object.entries(issue.details)
|
|
21
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
22
|
+
.join(' · '))}</div>`
|
|
23
|
+
: '';
|
|
24
|
+
return `<tr>
|
|
25
|
+
<td>${severityBadge(issue)}</td>
|
|
26
|
+
<td><span class="cat">${escapeHtml(issue.category)}</span></td>
|
|
27
|
+
<td class="msg">${escapeHtml(issue.message)}${details}</td>
|
|
28
|
+
<td><code>${escapeHtml(issue.route)}</code></td>
|
|
29
|
+
<td><span class="badge viewport">${escapeHtml(issue.viewport)}</span></td>
|
|
30
|
+
<td><code class="id">#${escapeHtml(issue.id)}</code></td>
|
|
31
|
+
</tr>`;
|
|
32
|
+
}
|
|
33
|
+
const STYLE = `
|
|
34
|
+
:root { color-scheme: dark; }
|
|
35
|
+
* { box-sizing: border-box; }
|
|
36
|
+
body { margin: 0; font: 15px/1.55 ui-sans-system, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
37
|
+
background: #0b0f17; color: #e2e8f0; }
|
|
38
|
+
header { padding: 40px 32px 24px; background: radial-gradient(900px 300px at 20% -50%, #1e293b, transparent), #0d1321;
|
|
39
|
+
border-bottom: 1px solid #1e293b; }
|
|
40
|
+
h1 { margin: 0 0 6px; font-size: 30px; } h2 { margin: 34px 0 12px; font-size: 19px; }
|
|
41
|
+
main { max-width: 1100px; margin: 0 auto; padding: 8px 32px 56px; }
|
|
42
|
+
.sub { color: #94a3b8; } code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px;
|
|
43
|
+
background: #1e293b; padding: 1px 6px; border-radius: 6px; }
|
|
44
|
+
.verdict { display: inline-block; margin-top: 14px; padding: 8px 16px; border-radius: 999px; font-weight: 700; }
|
|
45
|
+
.verdict.pass { background: #052e16; color: #4ade80; border: 1px solid #166534; }
|
|
46
|
+
.verdict.fail { background: #2e0a0a; color: #f87171; border: 1px solid #7f1d1d; }
|
|
47
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-top: 24px; }
|
|
48
|
+
.card { background: #111827; border: 1px solid #1f2937; border-radius: 14px; padding: 16px; }
|
|
49
|
+
.card .n { font-size: 26px; font-weight: 800; } .card .l { color: #94a3b8; font-size: 12.5px; text-transform: uppercase; letter-spacing: .06em; }
|
|
50
|
+
table { width: 100%; border-collapse: collapse; background: #0f172a; border: 1px solid #1e293b; border-radius: 12px; overflow: hidden; }
|
|
51
|
+
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #1e293b; vertical-align: top; }
|
|
52
|
+
th { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; background: #111827; }
|
|
53
|
+
tr:last-child td { border-bottom: none; }
|
|
54
|
+
.badge { --c: #94a3b8; color: var(--c); border: 1px solid var(--c); padding: 1px 8px; border-radius: 999px;
|
|
55
|
+
font-size: 11px; font-weight: 700; white-space: nowrap; }
|
|
56
|
+
.badge.viewport { --c: #7dd3fc; font-weight: 600; }
|
|
57
|
+
.cat { color: #c4b5fd; font-size: 12.5px; font-weight: 600; }
|
|
58
|
+
.msg { max-width: 520px; } .details { color: #64748b; font-size: 12.5px; margin-top: 3px; }
|
|
59
|
+
.id { color: #64748b; background: none; padding: 0; }
|
|
60
|
+
.ok { color: #4ade80; } .bad { color: #f87171; } .warn { color: #fbbf24; } .dim { color: #64748b; }
|
|
61
|
+
footer { color: #475569; font-size: 13px; padding: 0 32px 36px; max-width: 1100px; margin: 0 auto; }
|
|
62
|
+
`;
|
|
63
|
+
/**
|
|
64
|
+
* A sleek, self-contained (zero external assets) dark-mode HTML dashboard
|
|
65
|
+
* summarising one ghost-qa run — routes, interactions and every trap finding
|
|
66
|
+
* across desktop and mobile. Safe to attach to CI artifacts or PRs.
|
|
67
|
+
*/
|
|
68
|
+
export function formatHtmlReport(report) {
|
|
69
|
+
const { summary } = report;
|
|
70
|
+
const sortedIssues = [...report.issues].sort((a, b) => a.route.localeCompare(b.route) ||
|
|
71
|
+
SEVERITY_RANK(b.severity) - SEVERITY_RANK(a.severity));
|
|
72
|
+
const routeRows = report.routes
|
|
73
|
+
.map((route) => `<tr>
|
|
74
|
+
<td><code>${escapeHtml(route.path)}</code></td>
|
|
75
|
+
<td>${escapeHtml(route.title || '<span class="dim">(no title)</span>')}</td>
|
|
76
|
+
<td>${route.httpStatus ? `<code>${route.httpStatus}</code>` : '<span class="dim">—</span>'}</td>
|
|
77
|
+
<td>${route.viewports.map((v) => `<span class="badge viewport">${escapeHtml(v)}</span>`).join(' ')}</td>
|
|
78
|
+
<td>${route.issueCount === 0 ? '<span class="ok">✓ clean</span>' : `<span class="bad">${route.issueCount} issue${route.issueCount === 1 ? '' : 's'}</span>`}</td>
|
|
79
|
+
</tr>`)
|
|
80
|
+
.join('\n');
|
|
81
|
+
const interactionRows = report.interactions
|
|
82
|
+
.map((action) => `<tr>
|
|
83
|
+
<td><span class="cat">${escapeHtml(action.kind)}</span></td>
|
|
84
|
+
<td class="msg">${escapeHtml(action.target)}${action.reason ? `<div class="details">${escapeHtml(action.reason)}</div>` : ''}</td>
|
|
85
|
+
<td><code>${escapeHtml(action.route)}</code></td>
|
|
86
|
+
<td><span class="badge viewport">${escapeHtml(action.viewport)}</span></td>
|
|
87
|
+
<td>${action.outcome === 'passed' ? '<span class="ok">passed</span>' : action.outcome === 'failed' ? '<span class="bad">failed</span>' : '<span class="warn">skipped</span>'}</td>
|
|
88
|
+
</tr>`)
|
|
89
|
+
.join('\n');
|
|
90
|
+
const categoryRows = Object.entries(summary.byCategory)
|
|
91
|
+
.filter(([, count]) => count > 0)
|
|
92
|
+
.map(([category, count]) => `<tr><td><span class="cat">${escapeHtml(category)}</span></td><td>${count}</td></tr>`)
|
|
93
|
+
.join('\n');
|
|
94
|
+
return `<!doctype html>
|
|
95
|
+
<html lang="en">
|
|
96
|
+
<head>
|
|
97
|
+
<meta charset="utf-8">
|
|
98
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
99
|
+
<title>ghost-qa report — ${escapeHtml(report.targetUrl)}</title>
|
|
100
|
+
<style>${STYLE}</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<header>
|
|
104
|
+
<h1>👻 ghost-qa — Visual Evidence Report</h1>
|
|
105
|
+
<div class="sub">Target <code>${escapeHtml(report.targetUrl)}</code> · ghost-qa v${escapeHtml(report.version)} · ${(report.durationMs / 1000).toFixed(1)}s · ${escapeHtml(report.startedAt)}</div>
|
|
106
|
+
<div class="verdict ${report.passed ? 'pass' : 'fail'}">${report.passed ? '✔ PASS — no blocking browser failures' : '✖ FAIL — blocking browser failures found'}</div>
|
|
107
|
+
<div class="cards">
|
|
108
|
+
<div class="card"><div class="n">${summary.routesCrawled}</div><div class="l">Routes crawled</div></div>
|
|
109
|
+
<div class="card"><div class="n">${summary.interactionsTested}</div><div class="l">Interactions passed</div></div>
|
|
110
|
+
<div class="card"><div class="n">${summary.interactionsFailed}</div><div class="l">Interactions failed</div></div>
|
|
111
|
+
<div class="card"><div class="n">${summary.interactionsSkipped}</div><div class="l">Skipped (safety)</div></div>
|
|
112
|
+
<div class="card"><div class="n" style="color:#f87171">${summary.bySeverity.critical}</div><div class="l">Critical</div></div>
|
|
113
|
+
<div class="card"><div class="n" style="color:#fbbf24">${summary.bySeverity.major}</div><div class="l">Major</div></div>
|
|
114
|
+
<div class="card"><div class="n">${summary.totalIssues}</div><div class="l">Total issues</div></div>
|
|
115
|
+
</div>
|
|
116
|
+
</header>
|
|
117
|
+
<main>
|
|
118
|
+
<h2>Routes crawled</h2>
|
|
119
|
+
<table><thead><tr><th>Route</th><th>Title</th><th>Status</th><th>Viewports</th><th>Result</th></tr></thead>
|
|
120
|
+
<tbody>${routeRows || '<tr><td colspan="5" class="dim">No routes were visited.</td></tr>'}</tbody></table>
|
|
121
|
+
|
|
122
|
+
<h2>Issues by trap category</h2>
|
|
123
|
+
<table><thead><tr><th>Category</th><th>Issues</th></tr></thead>
|
|
124
|
+
<tbody>${categoryRows || '<tr><td colspan="2" class="ok">No issues trapped — clean run.</td></tr>'}</tbody></table>
|
|
125
|
+
|
|
126
|
+
<h2>Failures caught (${report.issues.length})</h2>
|
|
127
|
+
<table><thead><tr><th>Severity</th><th>Trap</th><th>Finding</th><th>Route</th><th>Viewport</th><th>Id</th></tr></thead>
|
|
128
|
+
<tbody>${sortedIssues.map(issueRow).join('\n') || '<tr><td colspan="6" class="ok">Nothing caught. The browser is happy. 👻✓</td></tr>'}</tbody></table>
|
|
129
|
+
|
|
130
|
+
<h2>Interactions executed (${report.interactions.length})</h2>
|
|
131
|
+
<table><thead><tr><th>Kind</th><th>Target</th><th>Route</th><th>Viewport</th><th>Outcome</th></tr></thead>
|
|
132
|
+
<tbody>${interactionRows || '<tr><td colspan="5" class="dim">Interactions were disabled for this run.</td></tr>'}</tbody></table>
|
|
133
|
+
</main>
|
|
134
|
+
<footer>Generated by ghost-qa ${escapeHtml(report.version)} · Five traps: console · network · layout · accessibility · performance · Re-run: <code>npx github:avivgerz-hue/ghost-qa test ${escapeHtml(report.targetUrl)}</code></footer>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
function SEVERITY_RANK(severity) {
|
|
140
|
+
return severity === 'critical' ? 3 : severity === 'major' ? 2 : 1;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=html-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-report.js","sourceRoot":"","sources":["../../../src/reporter/html-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GhostQAReport } from '../types.js';
|
|
2
|
+
/** Renders the human-facing CLI report printed at the end of `ghost-qa test`. */
|
|
3
|
+
export declare function formatTerminalSummary(report: GhostQAReport): string;
|
|
4
|
+
export declare function printTerminalSummary(report: GhostQAReport): void;
|
|
5
|
+
//# sourceMappingURL=terminal-summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-summary.d.ts","sourceRoot":"","sources":["../../../src/reporter/terminal-summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAW,MAAM,aAAa,CAAC;AA8B1D,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAmDnE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAEhE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const useColor = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined && process.env.TERM !== 'dumb';
|
|
2
|
+
const paint = (code) => (text) => (useColor() ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
3
|
+
const bold = paint('1');
|
|
4
|
+
const dim = paint('2');
|
|
5
|
+
const red = paint('31');
|
|
6
|
+
const yellow = paint('33');
|
|
7
|
+
const green = paint('32');
|
|
8
|
+
const cyan = paint('36');
|
|
9
|
+
const SEVERITY_ICON = {
|
|
10
|
+
critical: '✖',
|
|
11
|
+
major: '▲',
|
|
12
|
+
minor: '●',
|
|
13
|
+
};
|
|
14
|
+
function severityLabel(issue) {
|
|
15
|
+
const label = `${SEVERITY_ICON[issue.severity]} ${issue.severity.toUpperCase()}`;
|
|
16
|
+
if (issue.severity === 'critical')
|
|
17
|
+
return red(label);
|
|
18
|
+
if (issue.severity === 'major')
|
|
19
|
+
return yellow(label);
|
|
20
|
+
return dim(label);
|
|
21
|
+
}
|
|
22
|
+
function truncate(text, max) {
|
|
23
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
24
|
+
}
|
|
25
|
+
/** Renders the human-facing CLI report printed at the end of `ghost-qa test`. */
|
|
26
|
+
export function formatTerminalSummary(report) {
|
|
27
|
+
const { summary } = report;
|
|
28
|
+
const lines = [];
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push(bold('👻 ghost-qa — autonomous browser verification'));
|
|
31
|
+
lines.push(dim(`Target: ${report.targetUrl}`));
|
|
32
|
+
lines.push(dim(`Duration: ${(report.durationMs / 1000).toFixed(1)}s · Viewports: ${report.options.viewports.join(' + ')} · Depth: ${report.options.maxDepth}`));
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push(bold('Routes crawled'));
|
|
35
|
+
if (report.routes.length === 0) {
|
|
36
|
+
lines.push(dim(' (none — target did not load)'));
|
|
37
|
+
}
|
|
38
|
+
for (const route of report.routes) {
|
|
39
|
+
const status = route.httpStatus ? dim(` [${route.httpStatus}]`) : '';
|
|
40
|
+
const issues = route.issueCount > 0 ? red(` ${route.issueCount} issue${route.issueCount === 1 ? '' : 's'}`) : green(' ✓');
|
|
41
|
+
lines.push(` ${cyan(route.path.padEnd(28))} ${dim(route.title ? truncate(route.title, 32) : '')}${status}${issues}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push(bold('Coverage'));
|
|
45
|
+
lines.push(` Routes visited: ${summary.routesCrawled}`);
|
|
46
|
+
lines.push(` Interactions tested: ${summary.interactionsTested} passed · ${summary.interactionsFailed} failed · ${summary.interactionsSkipped} skipped (safety)`);
|
|
47
|
+
lines.push(` Issues: ${summary.totalIssues} total — ${red(`${summary.bySeverity.critical} critical`)}, ${yellow(`${summary.bySeverity.major} major`)}, ${dim(`${summary.bySeverity.minor} minor`)}`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
if (report.issues.length > 0) {
|
|
50
|
+
lines.push(bold('Failures caught'));
|
|
51
|
+
const sorted = [...report.issues].sort((a, b) => a.route.localeCompare(b.route));
|
|
52
|
+
for (const issue of sorted) {
|
|
53
|
+
lines.push(` ${severityLabel(issue)} ${dim(`[${issue.category}]`)} ${truncate(issue.message, 150)}`);
|
|
54
|
+
lines.push(dim(` ↳ ${issue.route} · ${issue.viewport} · #${issue.id}`));
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
if (report.passed) {
|
|
59
|
+
lines.push(green(bold('✔ GHOST-QA PASS — no blocking browser failures found. Safe to report done.')));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
lines.push(red(bold('✖ GHOST-QA FAIL — blocking browser failures found. Fix them and re-run before reporting done.')));
|
|
63
|
+
lines.push(dim(' Agent: read the Self-Healing Bug Report (ghost-qa-report.md / --json) and fix each issue by id.'));
|
|
64
|
+
}
|
|
65
|
+
lines.push('');
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
export function printTerminalSummary(report) {
|
|
69
|
+
process.stdout.write(`${formatTerminalSummary(report)}\n`);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=terminal-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-summary.js","sourceRoot":"","sources":["../../../src/reporter/terminal-summary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import { type QAIssue, type ViewportName } from '../types.js';
|
|
3
|
+
export interface A11yMetrics {
|
|
4
|
+
/** Buttons with no text content, aria-label, aria-labelledby or title. */
|
|
5
|
+
buttonsWithoutName: string[];
|
|
6
|
+
/** Links with no accessible name (icon-only links are the usual culprit). */
|
|
7
|
+
linksWithoutName: string[];
|
|
8
|
+
/** Images with no `alt` attribute at all (alt="" for decorative is fine). */
|
|
9
|
+
imagesMissingAlt: string[];
|
|
10
|
+
/** Form inputs with no <label>, aria-label or aria-labelledby. */
|
|
11
|
+
inputsMissingLabel: string[];
|
|
12
|
+
hasDocumentTitle: boolean;
|
|
13
|
+
hasHtmlLang: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const EMPTY_A11Y_METRICS: A11yMetrics;
|
|
16
|
+
/**
|
|
17
|
+
* Pure evaluator: DOM accessibility facts in, structured issues out.
|
|
18
|
+
* Fully unit-testable without a browser.
|
|
19
|
+
*/
|
|
20
|
+
export declare function evaluateA11yMetrics(metrics: A11yMetrics, route: string, viewport: ViewportName): QAIssue[];
|
|
21
|
+
/** Runs the in-page a11y probe against a live page and evaluates it. */
|
|
22
|
+
export declare class A11yTrap {
|
|
23
|
+
private readonly viewport;
|
|
24
|
+
constructor(viewport: ViewportName);
|
|
25
|
+
collect(page: Page, route: string): Promise<QAIssue[]>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=a11y-trap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y-trap.d.ts","sourceRoot":"","sources":["../../../src/traps/a11y-trap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,WAOhC,CAAC;AAIF;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,GACrB,OAAO,EAAE,CAwFX;AAoFD,wEAAwE;AACxE,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;gBAE5B,QAAQ,EAAE,YAAY;IAI5B,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAS7D"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { makeIssue } from '../types.js';
|
|
2
|
+
export const EMPTY_A11Y_METRICS = {
|
|
3
|
+
buttonsWithoutName: [],
|
|
4
|
+
linksWithoutName: [],
|
|
5
|
+
imagesMissingAlt: [],
|
|
6
|
+
inputsMissingLabel: [],
|
|
7
|
+
hasDocumentTitle: true,
|
|
8
|
+
hasHtmlLang: true,
|
|
9
|
+
};
|
|
10
|
+
const MAX_PER_KIND = 5;
|
|
11
|
+
/**
|
|
12
|
+
* Pure evaluator: DOM accessibility facts in, structured issues out.
|
|
13
|
+
* Fully unit-testable without a browser.
|
|
14
|
+
*/
|
|
15
|
+
export function evaluateA11yMetrics(metrics, route, viewport) {
|
|
16
|
+
const issues = [];
|
|
17
|
+
for (const descriptor of metrics.buttonsWithoutName.slice(0, MAX_PER_KIND)) {
|
|
18
|
+
issues.push(makeIssue({
|
|
19
|
+
category: 'accessibility',
|
|
20
|
+
severity: 'major',
|
|
21
|
+
type: 'missing-accessible-name',
|
|
22
|
+
message: `Button has no accessible name: ${descriptor}. Add visible text, aria-label, or aria-labelledby so screen readers and voice control can identify it.`,
|
|
23
|
+
route,
|
|
24
|
+
viewport,
|
|
25
|
+
details: { element: descriptor },
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
for (const descriptor of metrics.linksWithoutName.slice(0, MAX_PER_KIND)) {
|
|
29
|
+
issues.push(makeIssue({
|
|
30
|
+
category: 'accessibility',
|
|
31
|
+
severity: 'major',
|
|
32
|
+
type: 'missing-accessible-name',
|
|
33
|
+
message: `Link has no accessible name: ${descriptor}. Add text content or aria-label describing where the link goes.`,
|
|
34
|
+
route,
|
|
35
|
+
viewport,
|
|
36
|
+
details: { element: descriptor },
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
for (const src of metrics.imagesMissingAlt.slice(0, MAX_PER_KIND)) {
|
|
40
|
+
issues.push(makeIssue({
|
|
41
|
+
category: 'accessibility',
|
|
42
|
+
severity: 'major',
|
|
43
|
+
type: 'img-missing-alt',
|
|
44
|
+
message: `Image is missing an alt attribute: ${src}. Add descriptive alt text, or alt="" if the image is purely decorative.`,
|
|
45
|
+
route,
|
|
46
|
+
viewport,
|
|
47
|
+
details: { src },
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
for (const descriptor of metrics.inputsMissingLabel.slice(0, MAX_PER_KIND)) {
|
|
51
|
+
issues.push(makeIssue({
|
|
52
|
+
category: 'accessibility',
|
|
53
|
+
severity: 'major',
|
|
54
|
+
type: 'input-missing-label',
|
|
55
|
+
message: `Form input has no associated label: ${descriptor}. Connect a <label for="…">, wrap the input in a <label>, or add aria-label/aria-labelledby. A placeholder is not a label.`,
|
|
56
|
+
route,
|
|
57
|
+
viewport,
|
|
58
|
+
details: { element: descriptor },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
if (!metrics.hasDocumentTitle) {
|
|
62
|
+
issues.push(makeIssue({
|
|
63
|
+
category: 'accessibility',
|
|
64
|
+
severity: 'major',
|
|
65
|
+
type: 'missing-document-title',
|
|
66
|
+
message: 'Document has no <title> (or it is empty). Screen readers announce it first, tabs and search results depend on it. Add a unique, descriptive title per route.',
|
|
67
|
+
route,
|
|
68
|
+
viewport,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
if (!metrics.hasHtmlLang) {
|
|
72
|
+
issues.push(makeIssue({
|
|
73
|
+
category: 'accessibility',
|
|
74
|
+
severity: 'minor',
|
|
75
|
+
type: 'missing-html-lang',
|
|
76
|
+
message: 'Missing lang attribute on <html>. Screen readers cannot pick the right pronunciation rules without it, e.g. <html lang="en">.',
|
|
77
|
+
route,
|
|
78
|
+
viewport,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
return issues;
|
|
82
|
+
}
|
|
83
|
+
/** In-page probe: collects the accessibility facts from the live DOM. */
|
|
84
|
+
function probeA11y() {
|
|
85
|
+
const describe = (el) => {
|
|
86
|
+
const tag = el.tagName.toLowerCase();
|
|
87
|
+
const id = el.id ? `#${el.id}` : '';
|
|
88
|
+
const cls = typeof el.className === 'string' && el.className.trim()
|
|
89
|
+
? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}`
|
|
90
|
+
: '';
|
|
91
|
+
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
92
|
+
return `${tag}${id}${cls}${text ? ` "${text}"` : ''}`;
|
|
93
|
+
};
|
|
94
|
+
const accessibleName = (el) => {
|
|
95
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
96
|
+
if (labelledBy) {
|
|
97
|
+
const text = labelledBy
|
|
98
|
+
.split(/\s+/)
|
|
99
|
+
.map((id) => document.getElementById(id)?.textContent ?? '')
|
|
100
|
+
.join(' ')
|
|
101
|
+
.trim();
|
|
102
|
+
if (text)
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
return (el.getAttribute('aria-label') ||
|
|
106
|
+
el.getAttribute('title') ||
|
|
107
|
+
el.innerText ||
|
|
108
|
+
el.textContent ||
|
|
109
|
+
'')
|
|
110
|
+
.replace(/\s+/g, ' ')
|
|
111
|
+
.trim();
|
|
112
|
+
};
|
|
113
|
+
const visible = (el) => {
|
|
114
|
+
const style = window.getComputedStyle(el);
|
|
115
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
116
|
+
};
|
|
117
|
+
const buttonsWithoutName = [];
|
|
118
|
+
document.querySelectorAll('button, [role="button"]').forEach((el) => {
|
|
119
|
+
if (visible(el) && !accessibleName(el))
|
|
120
|
+
buttonsWithoutName.push(describe(el));
|
|
121
|
+
});
|
|
122
|
+
const linksWithoutName = [];
|
|
123
|
+
document.querySelectorAll('a[href]').forEach((el) => {
|
|
124
|
+
if (visible(el) && !accessibleName(el))
|
|
125
|
+
linksWithoutName.push(describe(el));
|
|
126
|
+
});
|
|
127
|
+
const imagesMissingAlt = [];
|
|
128
|
+
document.querySelectorAll('img').forEach((img) => {
|
|
129
|
+
if (!img.hasAttribute('alt')) {
|
|
130
|
+
imagesMissingAlt.push(img.currentSrc || img.src || img.id || '(image without src)');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const labelTargetIds = new Set(Array.from(document.querySelectorAll('label[for]')).map((l) => l.getAttribute('for')));
|
|
134
|
+
const inputsMissingLabel = [];
|
|
135
|
+
document
|
|
136
|
+
.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select')
|
|
137
|
+
.forEach((el) => {
|
|
138
|
+
if (!visible(el))
|
|
139
|
+
return;
|
|
140
|
+
const hasLabel = (el.id !== '' && labelTargetIds.has(el.id)) ||
|
|
141
|
+
el.closest('label') !== null ||
|
|
142
|
+
Boolean(el.getAttribute('aria-label')) ||
|
|
143
|
+
Boolean(el.getAttribute('aria-labelledby'));
|
|
144
|
+
if (!hasLabel)
|
|
145
|
+
inputsMissingLabel.push(describe(el));
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
buttonsWithoutName,
|
|
149
|
+
linksWithoutName,
|
|
150
|
+
imagesMissingAlt,
|
|
151
|
+
inputsMissingLabel,
|
|
152
|
+
hasDocumentTitle: document.title.trim().length > 0,
|
|
153
|
+
hasHtmlLang: (document.documentElement.getAttribute('lang') ?? '').trim().length > 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Runs the in-page a11y probe against a live page and evaluates it. */
|
|
157
|
+
export class A11yTrap {
|
|
158
|
+
viewport;
|
|
159
|
+
constructor(viewport) {
|
|
160
|
+
this.viewport = viewport;
|
|
161
|
+
}
|
|
162
|
+
async collect(page, route) {
|
|
163
|
+
let metrics;
|
|
164
|
+
try {
|
|
165
|
+
metrics = await page.evaluate(probeA11y);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return evaluateA11yMetrics(metrics, route, this.viewport);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=a11y-trap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y-trap.js","sourceRoot":"","sources":["../../../src/traps/a11y-trap.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAmC,MAAM,aAAa,CAAC;AAezE,MAAM,CAAC,MAAM,kBAAkB,GAAgB;IAC7C,kBAAkB,EAAE,EAAE;IACtB,gBAAgB,EAAE,EAAE;IACpB,gBAAgB,EAAE,EAAE;IACpB,kBAAkB,EAAE,EAAE;IACtB,gBAAgB,EAAE,IAAI;IACtB,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAoB,EACpB,KAAa,EACb,QAAsB;IAEtB,MAAM,MAAM,GAAc,EAAE,CAAC;IAE7B,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC;QAC3E,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,kCAAkC,UAAU,yGAAyG;YAC9J,KAAK;YACL,QAAQ;YACR,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE;SACjC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC;QACzE,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,gCAAgC,UAAU,kEAAkE;YACrH,KAAK;YACL,QAAQ;YACR,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE;SACjC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC;QAClE,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,sCAAsC,GAAG,0EAA0E;YAC5H,KAAK;YACL,QAAQ;YACR,OAAO,EAAE,EAAE,GAAG,EAAE;SACjB,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC;QAC3E,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,uCAAuC,UAAU,4HAA4H;YACtL,KAAK;YACL,QAAQ;YACR,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE;SACjC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EACL,8JAA8J;YAChK,KAAK;YACL,QAAQ;SACT,CAAC,CACH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CACT,SAAS,CAAC;YACR,QAAQ,EAAE,eAAe;YACzB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,mBAAmB;YACzB,OAAO,EACL,+HAA+H;YACjI,KAAK;YACL,QAAQ;SACT,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS;IAChB,MAAM,QAAQ,GAAG,CAAC,EAAW,EAAU,EAAE;QACvC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,MAAM,GAAG,GACP,OAAO,EAAE,CAAC,SAAS,KAAK,QAAQ,IAAI,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE;YACrD,CAAC,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;YAC9D,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,IAAI,GAAG,CAAC,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7E,OAAO,GAAG,GAAG,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACxD,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,CAAC,EAAW,EAAU,EAAE;QAC7C,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;QACtD,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,IAAI,GAAG,UAAU;iBACpB,KAAK,CAAC,KAAK,CAAC;iBACZ,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,WAAW,IAAI,EAAE,CAAC;iBAC3D,IAAI,CAAC,GAAG,CAAC;iBACT,IAAI,EAAE,CAAC;YACV,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC;QACxB,CAAC;QACD,OAAO,CACL,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC;YAC7B,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC;YACvB,EAAkB,CAAC,SAAS;YAC7B,EAAE,CAAC,WAAW;YACd,EAAE,CACH;aACE,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;aACpB,IAAI,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,EAAW,EAAW,EAAE;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,gBAAgB,CAAC,EAAiB,CAAC,CAAC;QACzD,OAAO,KAAK,CAAC,OAAO,KAAK,MAAM,IAAI,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC;IACnE,CAAC,CAAC;IAEF,MAAM,kBAAkB,GAAa,EAAE,CAAC;IACxC,QAAQ,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QAClE,IAAI,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YAAE,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QAClD,IAAI,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YAAE,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/C,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,IAAI,qBAAqB,CAAC,CAAC;QACtF,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CACtF,CAAC;IACF,MAAM,kBAAkB,GAAa,EAAE,CAAC;IACxC,QAAQ;SACL,gBAAgB,CAAC,wFAAwF,CAAC;SAC1G,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACd,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAAE,OAAO;QACzB,MAAM,QAAQ,GACZ,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YAC3C,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI;YAC5B,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YACtC,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ;YAAE,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEL,OAAO;QACL,kBAAkB;QAClB,gBAAgB;QAChB,gBAAgB;QAChB,kBAAkB;QAClB,gBAAgB,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAClD,WAAW,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;KACrF,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,OAAO,QAAQ;IACF,QAAQ,CAAe;IAExC,YAAY,QAAsB;QAChC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAU,EAAE,KAAa;QACrC,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5D,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import { type QAIssue, type ViewportName } from '../types.js';
|
|
3
|
+
export type ConsoleClassification = 'hydration-mismatch' | 'unhandled-rejection' | 'page-error' | 'console-error' | 'console-warning' | 'benign';
|
|
4
|
+
export declare function isHydrationError(text: string): boolean;
|
|
5
|
+
export declare function isUnhandledRejection(text: string): boolean;
|
|
6
|
+
export declare function isBenignConsoleNoise(text: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Pure classifier — maps a raw console/page-error string to a trap verdict.
|
|
9
|
+
* Kept side-effect free so it is fully unit-testable without a browser.
|
|
10
|
+
*/
|
|
11
|
+
export declare function classifyConsoleText(text: string, source?: 'console-error' | 'console-warning' | 'pageerror'): ConsoleClassification;
|
|
12
|
+
export declare function consoleIssueFromText(text: string, route: string, viewport: ViewportName, source?: 'console-error' | 'console-warning' | 'pageerror', stack?: string): QAIssue | null;
|
|
13
|
+
/**
|
|
14
|
+
* Attaches `pageerror` + `console` listeners to a Playwright page and
|
|
15
|
+
* accumulates structured issues. One trap instance per page.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ConsoleTrap {
|
|
18
|
+
readonly issues: QAIssue[];
|
|
19
|
+
private route;
|
|
20
|
+
private readonly viewport;
|
|
21
|
+
private detached;
|
|
22
|
+
constructor(viewport: ViewportName);
|
|
23
|
+
/** Tell the trap which route subsequent events belong to. */
|
|
24
|
+
setRoute(route: string): void;
|
|
25
|
+
attach(page: Page): void;
|
|
26
|
+
detach(): void;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=console-trap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-trap.d.ts","sourceRoot":"","sources":["../../../src/traps/console-trap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,MAAM,qBAAqB,GAC7B,oBAAoB,GACpB,qBAAqB,GACrB,YAAY,GACZ,eAAe,GACf,iBAAiB,GACjB,QAAQ,CAAC;AA+Bb,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,eAAe,GAAG,iBAAiB,GAAG,WAA6B,GAC1E,qBAAqB,CAOvB;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,EACtB,MAAM,GAAE,eAAe,GAAG,iBAAiB,GAAG,WAA6B,EAC3E,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,GAAG,IAAI,CAwDhB;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAM;IAChC,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,YAAY;IAIlC,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI7B,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IA4BxB,MAAM,IAAI,IAAI;CAGf"}
|