@@ -7,27 +7,10 @@ let eventSource = null;
77let savedEnvironments = { } ;
88let editingEnvName = null ;
99let 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
10376function 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
425400function 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 ( e r r o r | f a i l ( e d ) ? | e x c e p t i o n | t r a c e b a c k | a s s e r t ( i o n e r r o r ) ? ) \b | \b E \b | ✗ / i. test ( line ) ) return "error" ;
549- if ( / \b ( w a r n ( i n g ) ? | d e p r e c a t ) / i. test ( line ) ) return "warn" ;
550- if ( / \b ( p a s s e d | o k | s u c c e s s ) \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
566535async 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, "&" ) . replace ( / " / g, """ ) . replace ( / < / g, "<" ) ;
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
12691224function 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
13201242const 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-
13511261function 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
14141397function applyLatestRunToCards ( ) {
14151398 const history = getRunHistory ( ) ;
0 commit comments