Skip to content

Commit 4cd0d7c

Browse files
authored
Merge pull request #27 from MDA2AV/feature/show-request-response-details-on-hover-and-click
Add feature - ON mouse hover or click show a detailed info on request…
2 parents acfa55c + 2712f31 commit 4cd0d7c

46 files changed

Lines changed: 1238 additions & 57 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/probe.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ jobs:
156156
'connectionState': conn, 'reason': reason,
157157
'scored': scored,
158158
'durationMs': r.get('durationMs', 0),
159+
'rawRequest': r.get('rawRequest'),
160+
'rawResponse': r.get('rawResponse'),
161+
'behavioralNote': r.get('behavioralNote'),
159162
})
160163
161164
scored_results = [r for r in results if r['scored']]

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ nunit-*.xml
5959
docs/public/
6060
docs/.hugo_build.lock
6161
docs/resources/
62+
63+
# Probe data (generated by CI, pushed to latest-results branch)
64+
docs/static/probe/data.js
65+
66+
# Node
67+
node_modules/

docs/hugo.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ menu:
4141
weight: 8
4242
params:
4343
type: theme-toggle
44-
- name: GitHub
44+
- name: Discord
4545
weight: 9
46+
url: https://discord.gg/H84B5ZqDXR
47+
params:
48+
icon: discord
49+
- name: GitHub
50+
weight: 10
4651
url: https://github.com/MDA2AV/Http11Probe
4752
params:
4853
icon: github

docs/static/probe/render.js

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ window.ProbeRender = (function () {
55
var FAIL_BG = '#cf222e';
66
var SKIP_BG = '#656d76';
77
var EXPECT_BG = '#444c56';
8-
var pillCss = 'text-align:center;padding:2px 4px;font-size:11px;font-weight:600;color:#fff;border-radius:3px;min-width:28px;display:inline-block;line-height:18px;';
8+
var pillCss = 'text-align:center;padding:2px 4px;font-size:11px;font-weight:600;color:#fff;border-radius:3px;min-width:28px;display:inline-block;line-height:18px;cursor:default;';
9+
10+
function escapeAttr(s) {
11+
if (!s) return '';
12+
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
13+
}
914

1015
// Servers temporarily hidden from results (undergoing major changes)
1116
var BLACKLISTED_SERVERS = ['GenHTTP'];
@@ -44,10 +49,100 @@ window.ProbeRender = (function () {
4449
+ 'html.dark .probe-table tbody tr{border-bottom-color:#30363d}'
4550
+ 'html.dark .probe-server-row:hover{background:#161b22}'
4651
+ 'html.dark .probe-server-row.probe-row-active{background:#2a3a50 !important}'
47-
+ 'html.dark .probe-table thead a{color:#58a6ff !important}';
52+
+ 'html.dark .probe-table thead a{color:#58a6ff !important}'
53+
// 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}'
57+
+ '.probe-tooltip .probe-note{color:#f0c674;font-family:sans-serif;font-weight:600;font-size:11px;margin-bottom:6px;white-space:normal}'
58+
+ '.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}'
59+
+ '.probe-tooltip .probe-label:not(:first-child){margin-top:8px;padding-top:8px;border-top:1px solid #333}'
60+
// Modal (click)
61+
+ '.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}'
62+
+ '.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;'
64+
+ 'box-shadow:0 8px 32px rgba(0,0,0,0.5);line-height:1.5;position:relative;min-width:300px}'
65+
+ '.probe-modal .probe-note{color:#f0c674;font-family:sans-serif;font-weight:600;font-size:13px;margin-bottom:10px;white-space:normal}'
66+
+ '.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}'
67+
+ '.probe-modal .probe-label:not(:first-child){margin-top:12px;padding-top:12px;border-top:1px solid #333}'
68+
+ '.probe-modal-close{position:sticky;top:0;float:right;background:none;border:none;color:#808080;font-size:20px;'
69+
+ 'cursor:pointer;padding:0 4px;line-height:1;font-family:sans-serif}'
70+
+ '.probe-modal-close:hover{color:#fff}';
4871
var style = document.createElement('style');
4972
style.textContent = css;
5073
document.head.appendChild(style);
74+
75+
// Tooltip hover handler (delegated)
76+
var tip = null;
77+
document.addEventListener('mouseover', function (e) {
78+
var target = e.target.closest('[data-tooltip]');
79+
if (!target) return;
80+
if (tip) { tip.remove(); tip = null; }
81+
var text = target.getAttribute('data-tooltip');
82+
if (!text) return;
83+
tip = document.createElement('div');
84+
tip.className = 'probe-tooltip';
85+
var note = target.getAttribute('data-note');
86+
var req = target.getAttribute('data-request');
87+
var html = '';
88+
if (note) html += '<div class="probe-note">' + escapeAttr(note) + '</div>';
89+
if (req) html += '<div class="probe-label">Request</div>' + escapeAttr(req);
90+
if (text) html += '<div class="probe-label">Response</div>' + escapeAttr(text);
91+
tip.innerHTML = html;
92+
document.body.appendChild(tip);
93+
var rect = target.getBoundingClientRect();
94+
var tipRect = tip.getBoundingClientRect();
95+
var left = rect.left + rect.width / 2 - tipRect.width / 2;
96+
if (left < 4) left = 4;
97+
if (left + tipRect.width > window.innerWidth - 4) left = window.innerWidth - 4 - tipRect.width;
98+
var top = rect.top - tipRect.height - 6;
99+
if (top < 4) top = rect.bottom + 6;
100+
tip.style.left = left + 'px';
101+
tip.style.top = top + 'px';
102+
});
103+
document.addEventListener('mouseout', function (e) {
104+
var target = e.target.closest('[data-tooltip]');
105+
if (target && tip) { tip.remove(); tip = null; }
106+
});
107+
108+
// Modal click handler (delegated)
109+
document.addEventListener('click', function (e) {
110+
var target = e.target.closest('[data-tooltip]');
111+
if (!target) return;
112+
var text = target.getAttribute('data-tooltip');
113+
var req = target.getAttribute('data-request');
114+
if (!text && !req) return;
115+
// Dismiss hover tooltip
116+
if (tip) { tip.remove(); tip = null; }
117+
118+
var note = target.getAttribute('data-note');
119+
var html = '<button class="probe-modal-close" title="Close">&times;</button>';
120+
if (note) html += '<div class="probe-note">' + escapeAttr(note) + '</div>';
121+
if (req) html += '<div class="probe-label">Request</div>' + escapeAttr(req);
122+
if (text) html += '<div class="probe-label">Response</div>' + escapeAttr(text);
123+
124+
var overlay = document.createElement('div');
125+
overlay.className = 'probe-modal-overlay';
126+
var modal = document.createElement('div');
127+
modal.className = 'probe-modal';
128+
modal.innerHTML = html;
129+
overlay.appendChild(modal);
130+
document.body.appendChild(overlay);
131+
132+
// Close on X button
133+
modal.querySelector('.probe-modal-close').addEventListener('click', function () {
134+
overlay.remove();
135+
});
136+
// Close on overlay click (outside modal)
137+
overlay.addEventListener('click', function (ev) {
138+
if (ev.target === overlay) overlay.remove();
139+
});
140+
// Close on Escape
141+
function onKey(ev) {
142+
if (ev.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); }
143+
}
144+
document.addEventListener('keydown', onKey);
145+
});
51146
}
52147

53148
// ── Test ID → doc page URL mapping ─────────────────────────────
@@ -197,8 +292,14 @@ window.ProbeRender = (function () {
197292
return TEST_URLS[tid] || '';
198293
}
199294

200-
function pill(bg, label) {
201-
return '<span style="' + pillCss + 'background:' + bg + ';">' + label + '</span>';
295+
function pill(bg, label, tooltipRaw, tooltipNote, tooltipReq) {
296+
var extra = '';
297+
var hasData = tooltipRaw || tooltipReq;
298+
if (hasData) extra += ' data-tooltip="' + escapeAttr(tooltipRaw || '') + '"';
299+
if (tooltipNote) extra += ' data-note="' + escapeAttr(tooltipNote) + '"';
300+
if (tooltipReq) extra += ' data-request="' + escapeAttr(tooltipReq) + '"';
301+
var cursor = hasData ? 'cursor:pointer;' : 'cursor:default;';
302+
return '<span style="' + pillCss + cursor + 'background:' + bg + ';"' + extra + '>' + label + '</span>';
202303
}
203304

204305
function verdictBg(v) {
@@ -396,7 +497,7 @@ window.ProbeRender = (function () {
396497
t += '<td style="text-align:center;padding:2px 3px;' + opacity + '">' + pill(SKIP_BG, '\u2014') + '</td>';
397498
return;
398499
}
399-
t += '<td style="text-align:center;padding:2px 3px;' + opacity + '">' + pill(verdictBg(r.verdict), r.got) + '</td>';
500+
t += '<td style="text-align:center;padding:2px 3px;' + opacity + '">' + pill(verdictBg(r.verdict), r.got, r.rawResponse, r.behavioralNote, r.rawRequest) + '</td>';
400501
});
401502
t += '</tr>';
402503
});

src/Http11Probe.Cli/Reporting/JsonReporter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ public static string Generate(TestRunReport report)
4141
statusCode = r.Response?.StatusCode,
4242
connectionState = r.ConnectionState.ToString(),
4343
error = r.ErrorMessage,
44-
durationMs = r.Duration.TotalMilliseconds
44+
durationMs = r.Duration.TotalMilliseconds,
45+
rawRequest = r.RawRequest,
46+
rawResponse = r.Response?.RawResponse,
47+
behavioralNote = r.BehavioralNote
4548
})
4649
};
4750

src/Http11Probe/Response/HttpResponse.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public sealed class HttpResponse
77
public required string HttpVersion { get; init; }
88
public required IReadOnlyDictionary<string, string> Headers { get; init; }
99
public bool IsEmpty { get; init; }
10+
public string? RawResponse { get; init; }
11+
public string? Body { get; init; }
1012
}

src/Http11Probe/Response/ResponseParser.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,30 @@ public static class ResponseParser
7777
pos = nextLineEnd + 1;
7878
}
7979

80+
// Extract body after \r\n\r\n
81+
string? body = null;
82+
var headerEnd = text.IndexOf("\r\n\r\n", StringComparison.Ordinal);
83+
if (headerEnd >= 0)
84+
{
85+
var bodyStart = headerEnd + 4;
86+
if (bodyStart < text.Length)
87+
{
88+
var bodyText = text[bodyStart..];
89+
body = bodyText.Length > 4096 ? bodyText[..4096] : bodyText;
90+
}
91+
}
92+
93+
var rawResponse = text.Length > 8192 ? text[..8192] : text;
94+
8095
return new HttpResponse
8196
{
8297
StatusCode = statusCode,
8398
ReasonPhrase = reasonPhrase,
8499
HttpVersion = httpVersion,
85100
Headers = headers,
86-
IsEmpty = false
101+
IsEmpty = false,
102+
RawResponse = rawResponse,
103+
Body = body
87104
};
88105
}
89106
}

src/Http11Probe/Runner/TestRunner.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Text;
23
using Http11Probe.Client;
34
using Http11Probe.Response;
45
using Http11Probe.TestCases;
@@ -78,6 +79,7 @@ private async Task<TestResult> RunSingleAsync(TestCase testCase, TestContext con
7879

7980
// Send the primary payload
8081
var payload = testCase.PayloadFactory(context);
82+
var rawRequest = Encoding.ASCII.GetString(payload, 0, Math.Min(payload.Length, 8192));
8183
await client.SendAsync(payload);
8284

8385
// Read primary response
@@ -105,6 +107,7 @@ private async Task<TestResult> RunSingleAsync(TestCase testCase, TestContext con
105107
}
106108

107109
var verdict = testCase.Expected.Evaluate(response, connectionState);
110+
var behavioralNote = testCase.BehavioralAnalyzer?.Invoke(response);
108111

109112
return new TestResult
110113
{
@@ -113,6 +116,8 @@ private async Task<TestResult> RunSingleAsync(TestCase testCase, TestContext con
113116
Response = response,
114117
FollowUpResponse = followUpResponse,
115118
ConnectionState = connectionState,
119+
BehavioralNote = behavioralNote,
120+
RawRequest = rawRequest,
116121
Duration = sw.Elapsed
117122
};
118123
}

0 commit comments

Comments
 (0)