Skip to content

Commit c7afb48

Browse files
mariogregorclaude
andcommitted
feat: Performance-Budgets, Run-Vergleich, JS-Modularisierung
- Performance-Budgets: Status->Severity-Mapping (passed/warning/failed -> info/moderate/serious), Budget-Werte sichtbar im Output - Page-Size-Schwellen extrahiert nach _SIZE_THRESHOLDS, Severity passend zum Status - Run-Vergleich: Diff-Modal zeigt neu fehlgeschlagene/bestandene Tests vs. vorherigem Lauf, Test-Results werden in ep_test_history pro Run gespeichert - common.js extrahiert: Icon-Konstanten, escapeHtml/escapeAttr, api, setBtnLoading, setConnectionStatus, severityTooltip, classifyConsoleLine, renderConsoleOutput, formatDuration (117 Zeilen, vor app.js geladen) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ceb87c3 commit c7afb48

5 files changed

Lines changed: 325 additions & 113 deletions

File tree

static/css/style.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,77 @@ body, header, .card, .console-output, select, .url-input, .text-input, .btn-seco
16501650
height: 1.25rem;
16511651
}
16521652

1653+
/* ========== Run-Vergleich (Diff-Modal) ========== */
1654+
1655+
.diff-summary-row {
1656+
display: flex;
1657+
flex-wrap: wrap;
1658+
gap: 0.5rem;
1659+
margin-bottom: 0.5rem;
1660+
}
1661+
1662+
.diff-stat {
1663+
padding: 0.3rem 0.7rem;
1664+
border-radius: 999px;
1665+
font-size: 0.8rem;
1666+
font-weight: 600;
1667+
}
1668+
1669+
.diff-stat.diff-newly-failed { background: var(--sev-critical-bg); color: var(--sev-critical-fg); }
1670+
.diff-stat.diff-newly-passed { background: var(--suite-a11y-bg); color: var(--suite-a11y-fg); }
1671+
.diff-stat.diff-still-failing { background: var(--sev-serious-bg); color: var(--sev-serious-fg); }
1672+
.diff-stat.diff-new-tests { background: var(--sev-info-bg); color: var(--sev-info-fg); }
1673+
.diff-stat.diff-removed-tests { background: var(--sev-minor-bg); color: var(--sev-minor-fg); }
1674+
1675+
.diff-meta {
1676+
font-size: 0.8rem;
1677+
color: var(--text-muted);
1678+
margin-bottom: 1rem;
1679+
}
1680+
1681+
.diff-section {
1682+
margin-bottom: 1.25rem;
1683+
}
1684+
1685+
.diff-section h3 {
1686+
font-size: 0.95rem;
1687+
margin-bottom: 0.4rem;
1688+
color: var(--text);
1689+
}
1690+
1691+
.diff-list {
1692+
list-style: none;
1693+
padding: 0;
1694+
margin: 0;
1695+
}
1696+
1697+
.diff-item {
1698+
display: flex;
1699+
align-items: center;
1700+
gap: 0.5rem;
1701+
padding: 0.4rem 0.6rem;
1702+
border-radius: 6px;
1703+
margin-bottom: 0.2rem;
1704+
font-size: 0.875rem;
1705+
border-left: 3px solid transparent;
1706+
background: var(--bg);
1707+
}
1708+
1709+
.diff-item .name { flex: 1; }
1710+
1711+
.diff-item.newly-failed { border-left-color: var(--error); }
1712+
.diff-item.newly-passed { border-left-color: var(--success); }
1713+
.diff-item.still-failing { border-left-color: var(--warning); }
1714+
.diff-item.new-tests { border-left-color: var(--primary); }
1715+
.diff-item.removed-tests { border-left-color: var(--text-muted); opacity: 0.7; }
1716+
1717+
.diff-empty {
1718+
color: var(--text-muted);
1719+
font-style: italic;
1720+
padding: 1rem;
1721+
text-align: center;
1722+
}
1723+
16531724
/* ========== Drag&Drop Screenshots ========== */
16541725

16551726
.screenshot-thumb {

static/js/app.js

Lines changed: 90 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,10 @@ let eventSource = null;
77
let savedEnvironments = {};
88
let editingEnvName = null;
99
let runStartTime = null;
10+
let currentRunResults = [];
1011

11-
// SVG-Icons (Feather/Lucide-Stil, currentColor)
12-
const ICON_EDIT = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path></svg>';
13-
const ICON_TRASH = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
14-
const ICON_CHECK = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
15-
const ICON_X = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
16-
const ICON_WARNING = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
17-
const ICON_INFO = '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
18-
19-
// Button-Loading-Helper
20-
function setBtnLoading(btnOrId, loading) {
21-
const btn = typeof btnOrId === "string" ? document.getElementById(btnOrId) : btnOrId;
22-
if (!btn) return;
23-
if (loading) {
24-
btn.classList.add("is-loading");
25-
btn.disabled = true;
26-
} else {
27-
btn.classList.remove("is-loading");
28-
btn.disabled = false;
29-
}
30-
}
12+
// Helper, Icon-Konstanten, formatDuration, severityTooltip, renderConsoleOutput,
13+
// setBtnLoading, setConnectionStatus, escapeHtml, escapeAttr, api: siehe common.js
3114

3215
// ========== Initialisierung ==========
3316

@@ -88,16 +71,6 @@ function restoreFormFields() {
8871
}
8972
}
9073

91-
// ========== API-Aufrufe ==========
92-
93-
async function api(url, options = {}) {
94-
const resp = await fetch(url, {
95-
headers: { "Content-Type": "application/json" },
96-
...options,
97-
});
98-
return resp.json();
99-
}
100-
10174
// ========== URL und Credentials aus dem Eingabefeld ==========
10275

10376
function getUrl() {
@@ -420,9 +393,17 @@ function clearResults() {
420393
document.getElementById("testListA11y").innerHTML = "";
421394
document.getElementById("consoleOutput").textContent = "";
422395
document.getElementById("resultsSummary").innerHTML = "";
396+
currentRunResults = [];
397+
document.getElementById("btnDiff").style.display = "none";
423398
}
424399

425400
function addTestResult(result) {
401+
currentRunResults.push({
402+
name: result.name,
403+
outcome: result.outcome,
404+
suite: result.suite,
405+
});
406+
426407
const icon = result.outcome === "passed" ? "\u2713"
427408
: result.outcome === "failed" ? "\u2717"
428409
: "\u2014";
@@ -503,10 +484,16 @@ async function onTestsCompleted(data) {
503484
failed: data.failed || 0,
504485
total: total,
505486
duration_ms: durationMs,
487+
results: currentRunResults.slice(),
506488
});
507489
}
508490
renderAllSparklines();
509491

492+
// Diff-Button anzeigen, wenn Vergleichs-Lauf existiert
493+
const history = getRunHistory();
494+
document.getElementById("btnDiff").style.display =
495+
(!cancelled && history.length >= 2) ? "" : "none";
496+
510497
stopLiveBrowser();
511498

512499
// Jira-Export-Button einblenden falls Fehler vorhanden und Jira konfiguriert
@@ -543,24 +530,6 @@ async function loadConsoleOutput(runId) {
543530
}
544531
}
545532

546-
// Konsole zeilenweise mit Severity-Klassen rendern
547-
function classifyConsoleLine(line) {
548-
if (/\b(error|fail(ed)?|exception|traceback|assert(ionerror)?)\b|\bE \b|/i.test(line)) return "error";
549-
if (/\b(warn(ing)?|deprecat)/i.test(line)) return "warn";
550-
if (/\b(passed|ok|success)\b|/i.test(line)) return "success";
551-
return "info";
552-
}
553-
554-
function renderConsoleOutput(lines) {
555-
const out = document.getElementById("consoleOutput");
556-
if (!out) return;
557-
out.innerHTML = lines.map(line => {
558-
const cls = classifyConsoleLine(line);
559-
return `<span class="console-line-${cls}">${escapeHtml(line)}</span>`;
560-
}).join("\n");
561-
out.scrollTop = out.scrollHeight;
562-
}
563-
564533
// ========== Discovery ==========
565534

566535
async function runDiscovery() {
@@ -1250,20 +1219,6 @@ function renderDetectedForm(data) {
12501219
section.style.display = "block";
12511220
}
12521221

1253-
function escapeAttr(str) {
1254-
if (!str) return "";
1255-
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1256-
}
1257-
1258-
// ========== Hilfsfunktionen ==========
1259-
1260-
function escapeHtml(str) {
1261-
if (!str) return "";
1262-
const div = document.createElement("div");
1263-
div.textContent = str;
1264-
return div.innerHTML;
1265-
}
1266-
12671222
// ========== Theme-Toggle (Dark Mode) ==========
12681223

12691224
function toggleTheme() {
@@ -1282,39 +1237,6 @@ function syncThemeToggleState() {
12821237
btn.title = isDark ? "Zum hellen Modus wechseln" : "Zum dunklen Modus wechseln";
12831238
}
12841239

1285-
// ========== Connection-Status ==========
1286-
1287-
function setConnectionStatus(state, customLabel) {
1288-
const states = {
1289-
idle: { cls: "idle", text: "Bereit" },
1290-
running: { cls: "running", text: "Tests laufen..." },
1291-
discovering: { cls: "discovering", text: "Discovery läuft..." },
1292-
scanning: { cls: "scanning", text: "Scan läuft..." },
1293-
error: { cls: "error", text: "Fehler" },
1294-
cancelled: { cls: "cancelled", text: "Abgebrochen" },
1295-
success: { cls: "success", text: "Bereit" },
1296-
};
1297-
const s = states[state] || states.idle;
1298-
const label = customLabel || s.text;
1299-
const el = document.getElementById("connectionStatus");
1300-
if (!el) return;
1301-
el.innerHTML = `<span class="status-dot ${s.cls}"></span> ${escapeHtml(label)}`;
1302-
}
1303-
1304-
// ========== Severity-Tooltips ==========
1305-
1306-
function severityTooltip(severity) {
1307-
const tips = {
1308-
critical: "Kritisch — blockiert Nutzer komplett, sofort beheben.\nz.B. fehlender Alt-Text bei funktionalen Bildern, Tastaturfalle, Seite lädt nicht.",
1309-
serious: "Schwerwiegend — beeinträchtigt viele Nutzer, hohe Priorität.\nz.B. fehlendes Form-Label, broken Link, kein H1, fehlender <title>, fehlender Login-Button.",
1310-
moderate: "Mittel — Verbesserung empfohlen, kein direkter Blocker.\nz.B. unklare Linktexte, niedriger Kontrast, fehlende Meta-Description, horizontaler Overflow.",
1311-
minor: "Gering — kosmetisch oder Edge-Case, niedrige Priorität.\nz.B. unbenötigtes ARIA-Attribut, Performance-Wert nicht messbar.",
1312-
info: "Info — Hinweis ohne Bewertung, kein Handlungsbedarf.\nz.B. Anzahl Links auf der Seite, gefundene Open-Graph-Tags, Heading-Struktur.",
1313-
warning: "Warnung — auffällig, aber kein harter Fehler.\nz.B. langsame Ladezeit, kleine SEO-Optimierung möglich, Pre-Action-Fehler.",
1314-
};
1315-
return tips[severity] || "";
1316-
}
1317-
13181240
// ========== Run-History + Sparkline ==========
13191241

13201242
const HISTORY_KEY = "ep_test_history";
@@ -1336,18 +1258,6 @@ function addRunToHistory(run) {
13361258
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(list)); } catch (e) {}
13371259
}
13381260

1339-
function formatDuration(ms) {
1340-
if (ms == null || isNaN(ms) || ms < 0) return "--";
1341-
const totalSec = Math.round(ms / 1000);
1342-
if (totalSec < 60) return `${totalSec}s`;
1343-
const min = Math.floor(totalSec / 60);
1344-
const sec = totalSec % 60;
1345-
if (min < 60) return `${min}m ${sec.toString().padStart(2, "0")}s`;
1346-
const h = Math.floor(min / 60);
1347-
const m = min % 60;
1348-
return `${h}h ${m.toString().padStart(2, "0")}m`;
1349-
}
1350-
13511261
function renderAllSparklines() {
13521262
renderSparkline("cardLastRun", r => r.pct, { fixedMin: 0, fixedMax: 100, label: "Pass-Rate-Trend", fmt: v => `${Math.round(v)}%` });
13531263
renderSparkline("cardDuration", r => (r.duration_ms != null ? r.duration_ms : null), { label: "Dauer-Trend", fmt: v => formatDuration(v) });
@@ -1410,6 +1320,79 @@ function renderRunSparkline() {
14101320
renderAllSparklines();
14111321
}
14121322

1323+
// Vergleich der letzten beiden Läufe
1324+
function diffRuns(currentResults, previousResults) {
1325+
const prevMap = new Map((previousResults || []).map(r => [r.name, r.outcome]));
1326+
const curMap = new Map((currentResults || []).map(r => [r.name, r.outcome]));
1327+
1328+
const newlyFailed = [];
1329+
const newlyPassed = [];
1330+
const stillFailing = [];
1331+
const newTests = [];
1332+
1333+
for (const cur of currentResults || []) {
1334+
const prev = prevMap.get(cur.name);
1335+
if (prev === undefined) {
1336+
newTests.push(cur);
1337+
} else if (cur.outcome === "failed" && prev !== "failed") {
1338+
newlyFailed.push(cur);
1339+
} else if (cur.outcome === "passed" && prev === "failed") {
1340+
newlyPassed.push(cur);
1341+
} else if (cur.outcome === "failed" && prev === "failed") {
1342+
stillFailing.push(cur);
1343+
}
1344+
}
1345+
1346+
const removedTests = (previousResults || []).filter(p => !curMap.has(p.name));
1347+
1348+
return { newlyFailed, newlyPassed, stillFailing, newTests, removedTests };
1349+
}
1350+
1351+
function openDiffModal() {
1352+
const history = getRunHistory();
1353+
if (history.length < 2) return;
1354+
1355+
const current = history[history.length - 1];
1356+
const previous = history[history.length - 2];
1357+
const diff = diffRuns(current.results || [], previous.results || []);
1358+
1359+
const summaryEl = document.getElementById("diffSummary");
1360+
const contentEl = document.getElementById("diffContent");
1361+
1362+
summaryEl.innerHTML = `
1363+
<div class="diff-summary-row">
1364+
<span class="diff-stat diff-newly-failed">${diff.newlyFailed.length} neu fehlgeschlagen</span>
1365+
<span class="diff-stat diff-newly-passed">${diff.newlyPassed.length} neu bestanden</span>
1366+
<span class="diff-stat diff-still-failing">${diff.stillFailing.length} weiter fehlgeschlagen</span>
1367+
<span class="diff-stat diff-new-tests">${diff.newTests.length} neu</span>
1368+
<span class="diff-stat diff-removed-tests">${diff.removedTests.length} entfernt</span>
1369+
</div>
1370+
<div class="diff-meta">
1371+
Vergleich: ${new Date(previous.ts).toLocaleString("de-DE")}${new Date(current.ts).toLocaleString("de-DE")}
1372+
</div>
1373+
`;
1374+
1375+
const renderSection = (title, tests, cls) => {
1376+
if (!tests.length) return "";
1377+
const items = tests.map(t => {
1378+
const name = t.name.replace("test_", "").replace(/_/g, " ").replace(/^\w/, c => c.toUpperCase());
1379+
const suite = t.suite ? `<span class="suite-tag ${t.suite}">${t.suite.toUpperCase()}</span>` : "";
1380+
return `<li class="diff-item ${cls}"><span class="name">${escapeHtml(name)}</span>${suite}</li>`;
1381+
}).join("");
1382+
return `<div class="diff-section"><h3>${title} (${tests.length})</h3><ul class="diff-list">${items}</ul></div>`;
1383+
};
1384+
1385+
contentEl.innerHTML =
1386+
renderSection("Neu fehlgeschlagen", diff.newlyFailed, "newly-failed") +
1387+
renderSection("Neu bestanden", diff.newlyPassed, "newly-passed") +
1388+
renderSection("Weiterhin fehlgeschlagen", diff.stillFailing, "still-failing") +
1389+
renderSection("Neue Tests", diff.newTests, "new-tests") +
1390+
renderSection("Nicht mehr ausgeführt", diff.removedTests, "removed-tests") ||
1391+
'<p class="diff-empty">Keine Unterschiede zwischen den Läufen.</p>';
1392+
1393+
document.getElementById("diffModal").style.display = "flex";
1394+
}
1395+
14131396
// Initial-State der Stat-Cards aus History befüllen
14141397
function applyLatestRunToCards() {
14151398
const history = getRunHistory();

0 commit comments

Comments
 (0)