Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions DebugProbe.AspNetCore.Tests/Rendering/HtmlRendererEnvDiffTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using DebugProbe.AspNetCore.Internal.Rendering;
using DebugProbe.AspNetCore.Models;
using DebugProbe.AspNetCore.Options;
using DebugProbe.AspNetCore.Storage;
using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

namespace DebugProbe.AspNetCore.Tests.Rendering;

public class HtmlRendererEnvDiffTests
{
[Fact]
public void Render_index_page_with_AutoEnvironmentDiff_disabled_does_not_compare()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = false };
var store = new DebugEntryStore(options);

var entry1 = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};
var entry2 = new DebugEntry
{
Id = "2",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"error\"}"
};

var devEnv = new DebugEnvironment { Environment = "Development" };
var prodEnv = new DebugEnvironment { Environment = "Production" };

store.Add(entry1, devEnv);
store.Add(entry2, prodEnv);

// Act
var html = HtmlRenderer.RenderIndexPage(store.GetAll(), options);

// Assert
Assert.DoesNotContain("class=\"dbp-badge dbp-badge-envdiff\"", html);
}

[Fact]
public void Render_index_page_with_same_environment_shows_no_badge()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = true };
var store = new DebugEntryStore(options);

var entry1 = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};
var entry2 = new DebugEntry
{
Id = "2",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"error\"}"
};

var devEnv = new DebugEnvironment { Environment = "Development" };

store.Add(entry1, devEnv);
store.Add(entry2, devEnv);

// Act
var html = HtmlRenderer.RenderIndexPage(store.GetAll(), options);

// Assert
Assert.DoesNotContain("class=\"dbp-badge dbp-badge-envdiff\"", html);
}

[Fact]
public void Render_index_page_with_different_environments_and_identical_payloads_shows_no_badge()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = true };
var store = new DebugEntryStore(options);

var entry1 = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};
var entry2 = new DebugEntry
{
Id = "2",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};

var devEnv = new DebugEnvironment { Environment = "Development" };
var prodEnv = new DebugEnvironment { Environment = "Production" };

store.Add(entry1, devEnv);
store.Add(entry2, prodEnv);

// Act
var html = HtmlRenderer.RenderIndexPage(store.GetAll(), options);

// Assert
Assert.DoesNotContain("class=\"dbp-badge dbp-badge-envdiff\"", html);
}

[Fact]
public void Render_index_page_with_different_environments_and_differing_payloads_shows_badge()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = true };
var store = new DebugEntryStore(options);

var entry1 = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};
var entry2 = new DebugEntry
{
Id = "2",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"error\"}"
};

var devEnv = new DebugEnvironment { Environment = "Development" };
var prodEnv = new DebugEnvironment { Environment = "Production" };

store.Add(entry1, devEnv);
store.Add(entry2, prodEnv);

// Act
var html = HtmlRenderer.RenderIndexPage(store.GetAll(), options);

// Assert
Assert.Contains("class=\"dbp-badge dbp-badge-envdiff\"", html);
Assert.Contains("Payload differences detected between: Production, Development", html);
}

[Fact]
public void Route_normalization_matches_slash_and_query_string()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = true };
var store = new DebugEntryStore(options);

// entry1 path has trailing slash, entry2 path has query string. They should normalize to /api/users
var entry1 = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-5),
Method = "GET",
Path = "/api/users/",
ResponseBody = "{\"status\": \"ok\"}"
};
var entry2 = new DebugEntry
{
Id = "2",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Path = "/api/users?id=123",
Method = "GET",
ResponseBody = "{\"status\": \"error\"}"
};

var devEnv = new DebugEnvironment { Environment = "Development" };
var prodEnv = new DebugEnvironment { Environment = "Production" };

store.Add(entry1, devEnv);
store.Add(entry2, prodEnv);

// Act
var html = HtmlRenderer.RenderIndexPage(store.GetAll(), options);

// Assert
Assert.Contains("class=\"dbp-badge dbp-badge-envdiff\"", html);
}

[Fact]
public void Empty_store_and_single_entry_show_no_badge()
{
// Arrange
var options = new DebugProbeOptions { AutoEnvironmentDiff = true };
var store = new DebugEntryStore(options);

// Act & Assert (empty)
var htmlEmpty = HtmlRenderer.RenderIndexPage(store.GetAll(), options);
Assert.DoesNotContain("class=\"dbp-badge dbp-badge-envdiff\"", htmlEmpty);

// Add single entry
var entry = new DebugEntry
{
Id = "1",
Timestamp = DateTimeOffset.UtcNow,
Method = "GET",
Path = "/api/users",
ResponseBody = "{\"status\": \"ok\"}"
};
store.Add(entry, new DebugEnvironment { Environment = "Development" });

// Act & Assert (single entry)
var htmlSingle = HtmlRenderer.RenderIndexPage(store.GetAll(), options);
Assert.DoesNotContain("class=\"dbp-badge dbp-badge-envdiff\"", htmlSingle);
}
}
6 changes: 6 additions & 0 deletions DebugProbe.AspNetCore/Assets/css/debugprobe.css
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,12 @@ pre {
border: 1px solid rgba(255, 200, 0, 0.3);
}

.dbp-badge-envdiff {
background: rgba(231, 76, 60, 0.1);
color: #c0392b;
border: 1px solid rgba(231, 76, 60, 0.25);
}

/* =========================
Diff
========================= */
Expand Down
60 changes: 60 additions & 0 deletions DebugProbe.AspNetCore/Assets/js/debugprobe-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,63 @@ document.addEventListener("DOMContentLoaded", () => {
.replace(/'/g, "'");
}
});

// Global keyboard shortcuts listener for the DebugProbe dashboard.
document.addEventListener("keydown", (e) => {
// Ignore keydown when a modifier key is held (Ctrl/Cmd/Alt)
if (e.ctrlKey || e.metaKey || e.altKey) {
return;
}

// Ignore repeating events from holding down keys
if (e.repeat) {
return;
}

const key = e.key;

// "Escape" must always work, even inside input/textarea/contenteditable elements
if (key === "Escape" || key === "Esc") {
const backLink = document.querySelector('a[href="/debug"]') ||
Array.from(document.querySelectorAll("a")).find(a => a.textContent.includes("Back"));
if (backLink) {
backLink.click();
} else {
// If on the dashboard and in the search box, Esc blurs the input
const activeEl = document.activeElement;
if (activeEl && typeof activeEl.blur === "function") {
activeEl.blur();
}
}
return;
}

// Guard rail: skip all other shortcuts when in input/textarea/contenteditable
const activeEl = document.activeElement;
if (activeEl) {
const tag = activeEl.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || activeEl.isContentEditable) {
return;
}
}

switch (key) {
case "/": {
const searchInput = document.getElementById("requestSearch");
if (searchInput) {
e.preventDefault();
searchInput.focus();
}
break;
}
case "c":
case "C": {
const curlBtn = document.querySelector(".trace-card.request .curl-copy-btn");
if (curlBtn) {
curlBtn.click();
}
break;
}
}
});

Loading
Loading