Skip to content

Commit c457b47

Browse files
committed
Multiple UI/UX Improvements
1 parent 420bfcd commit c457b47

3 files changed

Lines changed: 197 additions & 26 deletions

File tree

docs/static/probe/render.js

Lines changed: 191 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,44 +51,85 @@ window.ProbeRender = (function () {
5151
+ 'html.dark .probe-server-row.probe-row-active{background:#2a3a50 !important}'
5252
+ 'html.dark .probe-table thead a{color:#58a6ff !important}'
5353
// Tooltip (hover)
54-
+ '.probe-tooltip{position:fixed;z-index:9999;background:#1c1c1c;color:#e0e0e0;font-family:monospace;font-size:11px;'
55-
+ 'white-space:pre;padding:8px 10px;border-radius:6px;max-width:500px;max-height:300px;overflow:auto;'
56-
+ 'pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,0.3);line-height:1.4}'
54+
+ '.probe-tooltip{position:fixed;z-index:10001;background:#1c1c1c;color:#e0e0e0;font-family:monospace;font-size:11px;'
55+
+ 'white-space:pre;padding:8px 10px;border-radius:6px;max-width:500px;max-height:60vh;overflow:auto;'
56+
+ 'box-shadow:0 4px 16px rgba(0,0,0,0.3);line-height:1.4}'
5757
+ '.probe-tooltip .probe-note{color:#f0c674;font-family:sans-serif;font-weight:600;font-size:11px;margin-bottom:6px;white-space:normal}'
5858
+ '.probe-tooltip .probe-label{color:#81a2be;font-family:sans-serif;font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px}'
5959
+ '.probe-tooltip .probe-label:not(:first-child){margin-top:8px;padding-top:8px;border-top:1px solid #333}'
6060
// Modal (click)
6161
+ '.probe-modal-overlay{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center}'
6262
+ '.probe-modal{background:#1c1c1c;color:#e0e0e0;font-family:monospace;font-size:12px;white-space:pre;'
63-
+ 'padding:16px 20px;border-radius:8px;max-width:700px;max-height:80vh;overflow:auto;'
63+
+ 'padding:16px 20px;border-radius:8px;max-width:90vw;max-height:85vh;overflow:auto;'
6464
+ 'box-shadow:0 8px 32px rgba(0,0,0,0.5);line-height:1.5;position:relative;min-width:300px}'
6565
+ '.probe-modal .probe-note{color:#f0c674;font-family:sans-serif;font-weight:600;font-size:13px;margin-bottom:10px;white-space:normal}'
6666
+ '.probe-modal .probe-label{color:#81a2be;font-family:sans-serif;font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}'
6767
+ '.probe-modal .probe-label:not(:first-child){margin-top:12px;padding-top:12px;border-top:1px solid #333}'
6868
+ '.probe-modal-close{position:sticky;top:0;float:right;background:none;border:none;color:#808080;font-size:20px;'
6969
+ 'cursor:pointer;padding:0 4px;line-height:1;font-family:sans-serif}'
70-
+ '.probe-modal-close:hover{color:#fff}';
70+
+ '.probe-modal-close:hover{color:#fff}'
71+
// Sticky first column — light
72+
+ '.probe-table .probe-sticky-col{position:sticky;left:0;z-index:2;background:#fff;box-shadow:2px 0 4px rgba(0,0,0,0.06)}'
73+
+ '.probe-table thead .probe-sticky-col{z-index:3}'
74+
+ 'tr[data-expected-row] .probe-sticky-col{background:#f6f8fa}'
75+
+ '.probe-server-row:hover .probe-sticky-col{background:#eef1f5}'
76+
+ '.probe-server-row.probe-row-active .probe-sticky-col{background:#c8ddf0}'
77+
// Sticky first column — dark
78+
+ 'html.dark .probe-table .probe-sticky-col{background:#1c2128;box-shadow:2px 0 4px rgba(0,0,0,0.2)}'
79+
+ 'html.dark tr[data-expected-row] .probe-sticky-col{background:#21262d}'
80+
+ 'html.dark .probe-server-row:hover .probe-sticky-col{background:#161b22}'
81+
+ 'html.dark .probe-server-row.probe-row-active .probe-sticky-col{background:#2a3a50}'
82+
// Collapsible groups
83+
+ '.probe-group-header{cursor:pointer;user-select:none;display:flex;align-items:center;gap:8px}'
84+
+ '.probe-group-chevron{display:inline-block;transition:transform 0.2s;font-size:0.8em}'
85+
+ '.probe-group-chevron.collapsed{transform:rotate(-90deg)}'
86+
+ '.probe-group-body{overflow:hidden;transition:max-height 0.3s ease,opacity 0.3s ease;max-height:10000px;opacity:1}'
87+
+ '.probe-group-body.collapsed{max-height:0;opacity:0}'
88+
+ '.probe-toggle-all{display:inline-block;padding:4px 12px;font-size:12px;font-weight:600;border-radius:20px;cursor:pointer;'
89+
+ 'border:1px solid #d0d7de;background:#f6f8fa;color:#24292f;margin-bottom:8px;transition:all 0.15s}'
90+
+ '.probe-toggle-all:hover{background:#eef1f5}'
91+
+ 'html.dark .probe-toggle-all{border-color:#30363d;background:#21262d;color:#c9d1d9}'
92+
+ 'html.dark .probe-toggle-all:hover{background:#30363d}';
7193
var style = document.createElement('style');
7294
style.textContent = css;
7395
document.head.appendChild(style);
7496

97+
// Truncation detection for raw request/response (8192-byte cap)
98+
function isTruncated(raw) {
99+
if (!raw || raw.indexOf('[Truncated') !== -1) return false;
100+
return raw.length > 500 && raw.charAt(raw.length - 1) !== '\n';
101+
}
102+
75103
// Tooltip hover handler (delegated)
76104
var tip = null;
105+
var tipTrigger = null;
106+
function dismissTip() { if (tip) { tip.remove(); tip = null; tipTrigger = null; } }
77107
document.addEventListener('mouseover', function (e) {
108+
// If mouse moved into the tooltip itself, keep it open
109+
if (tip && tip.contains(e.target)) return;
78110
var target = e.target.closest('[data-tooltip]');
79-
if (!target) return;
80-
if (tip) { tip.remove(); tip = null; }
111+
if (!target) { dismissTip(); return; }
112+
if (target === tipTrigger) return; // already showing for this element
113+
dismissTip();
81114
var text = target.getAttribute('data-tooltip');
82115
if (!text) return;
116+
tipTrigger = target;
83117
tip = document.createElement('div');
84118
tip.className = 'probe-tooltip';
85119
var note = target.getAttribute('data-note');
86120
var req = target.getAttribute('data-request');
121+
var truncated = isTruncated(req) || isTruncated(text);
87122
var html = '';
123+
if (truncated) html += '<div style="color:#f0c674;font-family:sans-serif;font-weight:600;font-size:10px;margin-bottom:6px;white-space:normal;">[Truncated \u2014 payload exceeds display limit]</div>';
88124
if (note) html += '<div class="probe-note">' + escapeAttr(note) + '</div>';
89125
if (req) html += '<div class="probe-label">Request</div>' + escapeAttr(req);
90126
if (text) html += '<div class="probe-label">Response</div>' + escapeAttr(text);
91127
tip.innerHTML = html;
128+
// Dismiss when mouse leaves the tooltip
129+
tip.addEventListener('mouseleave', function (ev) {
130+
if (tipTrigger && tipTrigger.contains(ev.relatedTarget)) return;
131+
dismissTip();
132+
});
92133
document.body.appendChild(tip);
93134
var rect = target.getBoundingClientRect();
94135
var tipRect = tip.getBoundingClientRect();
@@ -102,7 +143,10 @@ window.ProbeRender = (function () {
102143
});
103144
document.addEventListener('mouseout', function (e) {
104145
var target = e.target.closest('[data-tooltip]');
105-
if (target && tip) { tip.remove(); tip = null; }
146+
if (!target || target !== tipTrigger) return;
147+
// Don't dismiss if mouse moved into the tooltip
148+
if (tip && (e.relatedTarget === tip || tip.contains(e.relatedTarget))) return;
149+
if (target && tip) { dismissTip(); }
106150
});
107151

108152
// Modal click handler (delegated)
@@ -113,10 +157,12 @@ window.ProbeRender = (function () {
113157
var req = target.getAttribute('data-request');
114158
if (!text && !req) return;
115159
// Dismiss hover tooltip
116-
if (tip) { tip.remove(); tip = null; }
160+
dismissTip();
117161

118162
var note = target.getAttribute('data-note');
163+
var truncated = isTruncated(req) || isTruncated(text);
119164
var html = '<button class="probe-modal-close" title="Close">&times;</button>';
165+
if (truncated) html += '<div style="color:#f0c674;font-family:sans-serif;font-weight:600;font-size:12px;margin-bottom:8px;white-space:normal;">[Truncated \u2014 payload exceeds display limit]</div>';
120166
if (note) html += '<div class="probe-note">' + escapeAttr(note) + '</div>';
121167
if (req) html += '<div class="probe-label">Request</div>' + escapeAttr(req);
122168
if (text) html += '<div class="probe-label">Response</div>' + escapeAttr(text);
@@ -472,7 +518,9 @@ window.ProbeRender = (function () {
472518
el.innerHTML = html;
473519
}
474520

475-
function renderTable(targetId, categoryKey, ctx, testIdFilter) {
521+
var CAT_LABELS = { Compliance: 'Compliance', Smuggling: 'Smuggling', MalformedInput: 'Malformed Input', Normalization: 'Normalization' };
522+
523+
function renderTable(targetId, categoryKey, ctx, testIdFilter, tableLabel) {
476524
injectScrollStyle();
477525
var el = document.getElementById(targetId);
478526
if (!el) return;
@@ -500,7 +548,7 @@ window.ProbeRender = (function () {
500548

501549
// Column header row (diagonal labels)
502550
t += '<thead><tr>';
503-
t += '<th style="padding:4px 8px;text-align:left;vertical-align:bottom;min-width:100px;"></th>';
551+
t += '<th class="probe-sticky-col" style="padding:4px 8px;text-align:left;vertical-align:bottom;min-width:100px;"></th>';
504552
orderedTests.forEach(function (tid, i) {
505553
var first = lookup[names[0]][tid];
506554
var isUnscored = first.scored === false;
@@ -520,8 +568,8 @@ window.ProbeRender = (function () {
520568
t += '</tr></thead><tbody>';
521569

522570
// Expected row
523-
t += '<tr style="background:#f6f8fa;">';
524-
t += '<td style="padding:4px 8px;font-weight:700;font-size:11px;color:#656d76;">Expected</td>';
571+
t += '<tr data-expected-row style="background:#f6f8fa;">';
572+
t += '<td class="probe-sticky-col" style="padding:4px 8px;font-weight:700;font-size:11px;color:#656d76;">Expected</td>';
525573
orderedTests.forEach(function (tid) {
526574
var first = lookup[names[0]][tid];
527575
var isUnscored = first.scored === false;
@@ -534,14 +582,14 @@ window.ProbeRender = (function () {
534582
var serverLangs = {};
535583
if (ctx.servers) ctx.servers.forEach(function (sv) { serverLangs[sv.name] = sv.language; });
536584
names.forEach(function (n) {
537-
t += '<tr class="probe-server-row">';
585+
t += '<tr class="probe-server-row" data-server="' + escapeAttr(n) + '">';
538586
var lang = serverLangs[n];
539587
var langSuffix = lang ? ' <span style="font-weight:400;color:#656d76;font-size:10px;">(' + lang + ')</span>' : '';
540588
var srvUrl = serverUrl(n);
541589
var srvName = srvUrl
542590
? '<a href="' + srvUrl + '" style="color:inherit;text-decoration:none;" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + n + '</a>'
543591
: n;
544-
t += '<td style="padding:4px 8px;font-weight:600;font-size:12px;white-space:nowrap;">' + srvName + langSuffix + '</td>';
592+
t += '<td class="probe-sticky-col" style="padding:4px 8px;font-weight:600;font-size:12px;white-space:nowrap;">' + srvName + langSuffix + '</td>';
545593
orderedTests.forEach(function (tid) {
546594
var r = lookup[n] && lookup[n][tid];
547595
var isUnscored = lookup[names[0]][tid].scored === false;
@@ -561,16 +609,132 @@ window.ProbeRender = (function () {
561609
}
562610
el.innerHTML = t;
563611

564-
// Row click-to-highlight (one at a time)
612+
// Row click → detail popup
565613
var rows = el.querySelectorAll('.probe-server-row');
566614
rows.forEach(function (row) {
567615
row.addEventListener('click', function (e) {
568-
if (e.target.closest('a')) return;
569-
var wasActive = row.classList.contains('probe-row-active');
570-
rows.forEach(function (r) { r.classList.remove('probe-row-active'); });
571-
if (!wasActive) row.classList.add('probe-row-active');
616+
if (e.target.closest('a') || e.target.closest('[data-tooltip]')) return;
617+
618+
var svName = row.getAttribute('data-server');
619+
if (!svName) return;
620+
621+
// Build vertical detail table for this server
622+
var sUrl = serverUrl(svName);
623+
var titleHtml = sUrl
624+
? '<a href="' + sUrl + '" style="color:#58a6ff;text-decoration:underline;text-underline-offset:2px;">' + escapeAttr(svName) + '</a>'
625+
: escapeAttr(svName);
626+
627+
var pass = 0, warn = 0, fail = 0;
628+
orderedTests.forEach(function (tid) {
629+
var r = lookup[svName] && lookup[svName][tid];
630+
if (!r || r.scored === false) return;
631+
if (r.verdict === 'Pass') pass++;
632+
else if (r.verdict === 'Warn') warn++;
633+
else fail++;
634+
});
635+
636+
var displayLabel = tableLabel || CAT_LABELS[categoryKey] || categoryKey;
637+
var h = '<button class="probe-modal-close" title="Close">&times;</button>';
638+
h += '<div style="font-size:11px;font-weight:600;color:#81a2be;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px;">' + escapeAttr(displayLabel) + '</div>';
639+
h += '<div style="font-size:15px;font-weight:700;margin-bottom:4px;">' + titleHtml + '</div>';
640+
h += '<div style="display:flex;gap:12px;margin-bottom:12px;font-size:12px;font-weight:600;">';
641+
h += '<span style="color:' + PASS_BG + ';">' + pass + ' Pass</span>';
642+
if (warn > 0) h += '<span style="color:' + WARN_BG + ';">' + warn + ' Warn</span>';
643+
if (fail > 0) h += '<span style="color:' + FAIL_BG + ';">' + fail + ' Fail</span>';
644+
h += '</div>';
645+
646+
h += '<table style="border-collapse:collapse;width:100%;font-size:12px;">';
647+
h += '<thead><tr style="border-bottom:1px solid #333;">';
648+
h += '<th style="padding:4px 8px;text-align:left;color:#81a2be;">Test</th>';
649+
h += '<th style="padding:4px 8px;text-align:center;color:#81a2be;">Expected</th>';
650+
h += '<th style="padding:4px 8px;text-align:center;color:#81a2be;">Got</th>';
651+
h += '<th style="padding:4px 8px;text-align:left;color:#81a2be;">Description</th>';
652+
h += '</tr></thead><tbody>';
653+
654+
orderedTests.forEach(function (tid) {
655+
var first = lookup[names[0]][tid];
656+
var r = lookup[svName] && lookup[svName][tid];
657+
var isUnscored = first.scored === false;
658+
var opacity = isUnscored ? 'opacity:0.55;' : '';
659+
var shortLabel = tid.replace(/^(RFC\d+-[\d.]+-|COMP-|SMUG-|MAL-|NORM-)/, '');
660+
var url = testUrl(tid);
661+
var testLink = url
662+
? '<a href="' + url + '" style="color:#58a6ff;text-decoration:underline;text-underline-offset:2px;">' + shortLabel + '</a>'
663+
: shortLabel;
664+
if (isUnscored) testLink += '*';
665+
666+
var gotCell;
667+
if (!r) {
668+
gotCell = pill(SKIP_BG, '\u2014');
669+
} else {
670+
gotCell = pill(verdictBg(r.verdict), r.got, r.rawResponse, r.behavioralNote, r.rawRequest);
671+
}
672+
673+
h += '<tr style="border-bottom:1px solid #2a2f38;' + opacity + '">';
674+
h += '<td style="padding:4px 8px;font-weight:600;white-space:nowrap;">' + testLink + '</td>';
675+
h += '<td style="text-align:center;padding:2px 4px;">' + pill(EXPECT_BG, first.expected.replace(/ or close/g, '/\u2715').replace(/\//g, '/\u200B')) + '</td>';
676+
h += '<td style="text-align:center;padding:2px 4px;">' + gotCell + '</td>';
677+
h += '<td style="padding:4px 8px;color:#999;white-space:normal;max-width:300px;">' + (first.description || '') + '</td>';
678+
h += '</tr>';
679+
});
680+
h += '</tbody></table>';
681+
682+
var overlay = document.createElement('div');
683+
overlay.className = 'probe-modal-overlay';
684+
var modal = document.createElement('div');
685+
modal.className = 'probe-modal';
686+
modal.style.maxWidth = '800px';
687+
modal.style.whiteSpace = 'normal';
688+
modal.innerHTML = h;
689+
overlay.appendChild(modal);
690+
document.body.appendChild(overlay);
691+
692+
modal.querySelector('.probe-modal-close').addEventListener('click', function () { overlay.remove(); });
693+
overlay.addEventListener('click', function (ev) { if (ev.target === overlay) overlay.remove(); });
694+
function onKey(ev) {
695+
if (ev.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); }
696+
}
697+
document.addEventListener('keydown', onKey);
698+
});
699+
});
700+
}
701+
702+
// ── Collapsible-group wiring helper ────────────────────────────
703+
function wireCollapsible(el, targetId) {
704+
var headers = el.querySelectorAll('.probe-group-header');
705+
headers.forEach(function (hdr) {
706+
hdr.addEventListener('click', function () {
707+
var groupId = hdr.getAttribute('data-group');
708+
var body = document.getElementById(groupId);
709+
var chevron = hdr.querySelector('.probe-group-chevron');
710+
if (body) body.classList.toggle('collapsed');
711+
if (chevron) chevron.classList.toggle('collapsed');
712+
updateToggleAllLabel(el, targetId);
572713
});
573714
});
715+
var toggleBtn = el.querySelector('.probe-toggle-all');
716+
if (toggleBtn) {
717+
toggleBtn.addEventListener('click', function () {
718+
var bodies = el.querySelectorAll('.probe-group-body');
719+
var chevrons = el.querySelectorAll('.probe-group-chevron');
720+
var allCollapsed = Array.prototype.every.call(bodies, function (b) { return b.classList.contains('collapsed'); });
721+
bodies.forEach(function (b) {
722+
if (allCollapsed) b.classList.remove('collapsed'); else b.classList.add('collapsed');
723+
});
724+
chevrons.forEach(function (c) {
725+
if (allCollapsed) c.classList.remove('collapsed'); else c.classList.add('collapsed');
726+
});
727+
updateToggleAllLabel(el, targetId);
728+
});
729+
}
730+
}
731+
732+
function updateToggleAllLabel(container, targetId) {
733+
var btn = container.querySelector('.probe-toggle-all[data-target="' + targetId + '"]');
734+
if (!btn) return;
735+
var bodies = container.querySelectorAll('.probe-group-body');
736+
var allCollapsed = Array.prototype.every.call(bodies, function (b) { return b.classList.contains('collapsed'); });
737+
btn.textContent = allCollapsed ? 'Expand All' : 'Collapse All';
574738
}
575739

576740
// ── Sub-table renderer ─────────────────────────────────────────
@@ -594,17 +758,20 @@ window.ProbeRender = (function () {
594758
allGroups.push({ key: 'other', label: 'Other', testIds: ungrouped });
595759
}
596760

597-
var html = '';
761+
var html = '<button class="probe-toggle-all" data-target="' + targetId + '">Collapse All</button>';
598762
allGroups.forEach(function (g) {
599763
var divId = targetId + '-' + g.key;
600-
html += '<h3 style="margin-top:1.5em;margin-bottom:0.3em;">' + g.label + '</h3>';
601-
html += '<div id="' + divId + '"></div>';
764+
html += '<h3 class="probe-group-header" data-group="' + divId + '" style="margin-top:1.5em;margin-bottom:0.3em;">'
765+
+ '<span class="probe-group-chevron">\u25BC</span>' + g.label + '</h3>';
766+
html += '<div class="probe-group-body" id="' + divId + '"></div>';
602767
});
603768
el.innerHTML = html;
769+
var catLabel = CAT_LABELS[categoryKey] || categoryKey;
604770
allGroups.forEach(function (g) {
605771
var divId = targetId + '-' + g.key;
606-
renderTable(divId, categoryKey, ctx, g.testIds);
772+
renderTable(divId, categoryKey, ctx, g.testIds, catLabel + ' \u2014 ' + g.label);
607773
});
774+
wireCollapsible(el, targetId);
608775
}
609776

610777
// ── Language filter ────────────────────────────────────────────

src/Http11Probe/Response/ResponseParser.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ public static class ResponseParser
9090
}
9191
}
9292

93-
var rawResponse = text.Length > 8192 ? text[..8192] : text;
93+
var rawResponse = text.Length > 8192
94+
? text[..8192] + $"\n\n[Truncated — showing 8,192 of {text.Length:N0} bytes]"
95+
: text;
9496

9597
return new HttpResponse
9698
{

src/Http11Probe/Runner/TestRunner.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ private async Task<TestResult> RunSingleAsync(TestCase testCase, TestContext con
7979

8080
// Send the primary payload
8181
var payload = testCase.PayloadFactory(context);
82-
var rawRequest = Encoding.ASCII.GetString(payload, 0, Math.Min(payload.Length, 8192));
82+
var rawRequest = payload.Length > 8192
83+
? Encoding.ASCII.GetString(payload, 0, 8192) + $"\n\n[Truncated — showing 8,192 of {payload.Length:N0} bytes]"
84+
: Encoding.ASCII.GetString(payload);
8385
await client.SendAsync(payload);
8486

8587
// Read primary response

0 commit comments

Comments
 (0)