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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- test: enforce JavaScript coverage gates, extend mutation coverage to new UI modules, and simplify N+1 detection, redaction, rendering, and panels.
- feat(ui): add framework-neutral Vite snapshots, summary and detail rendering, consistent shell density, and labeled card-contained sidebar groups for adapter-provided extension panels.
- refactor(ui): replace the installed-extension tag cloud with a compact Composer namespace ledger.
- feat(ui): improve Request disclosures, body limits, history navigation, and table spacing.
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion resources/assets/dist/js/debug.min.js

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions resources/src/core/history-cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,33 @@
return "other";
}

/**
* Keep primary Request links aligned with the row represented by the
* client-side history cursor. The `auto` variant preserves compatibility
* with captures created before the Request panel was available.
*/
function updateRequestLinks(tag) {
if (!tag) {
return;
}

var links = document.querySelectorAll(
'.yii-debug-nav-link[href*="panel=request"], .yii-debug-nav-link[href*="panel=auto"]',
);

for (var li = 0; li < links.length; li++) {
var target = new URL(
links[li].getAttribute("href"),
window.location.href,
);
target.searchParams.set("tag", tag);
links[li].setAttribute(
"href",
target.pathname + target.search + target.hash,
);
}
}

function update() {
rows.forEach(function (r, i) {
r.classList.toggle("is-cursor", i === cursor);
Expand Down Expand Up @@ -134,6 +161,8 @@
);
}

updateRequestLinks(snap.tag);

var newestBtn = section.querySelector('[data-yii-debug-cursor="newest"]');
var newerBtn = section.querySelector('[data-yii-debug-cursor="newer"]');
var olderBtn = section.querySelector('[data-yii-debug-cursor="older"]');
Expand Down
3 changes: 2 additions & 1 deletion resources/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@
* a gap by accident. Separate every section from what precedes it, and let
* the first one start at the top of the panel.
*/
.yii-debug-main > * + :is(h2, .yii-debug-disclosure) {
.yii-debug-main > * + :is(h2, .yii-debug-disclosure),
.yii-debug-tab-panel > .yii-debug-table-wrap + .yii-debug-disclosure {
margin-top: var(--yii-debug-space-5);
}

Expand Down
44 changes: 43 additions & 1 deletion resources/tests/history-cursor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ var card = {
},
};

function navLink(href) {
return {
attributes: { href },
getAttribute(name) {
return this.attributes[name] ?? null;
},
setAttribute(name, value) {
this.attributes[name] = value;
},
};
}

var requestLink = navLink(
"/debug/view?tag=tag-1&panel=request&yii_debug_theme=dark",
);
var automaticLink = navLink("/debug/view?tag=tag-1&panel=auto");

var section = {
attributes: { "data-yii-debug-cursor-init": "tag-3" },
getAttribute(name) {
Expand Down Expand Up @@ -204,11 +221,22 @@ globalThis.document = {
return selector === "[data-yii-debug-history-cursor]" ? section : null;
},
querySelectorAll(selector) {
return selector === "tr[data-yii-debug-tag]" ? rows : [];
if (selector === "tr[data-yii-debug-tag]") {
return rows;
}
if (
selector ===
'.yii-debug-nav-link[href*="panel=request"], .yii-debug-nav-link[href*="panel=auto"]'
) {
return [requestLink, automaticLink];
}

return [];
},
};
globalThis.window = {
innerHeight: 800,
location: { href: "https://example.test/debug" },
scrollY: 100,
scrollTo(options) {
scrolls.push(options);
Expand Down Expand Up @@ -251,6 +279,14 @@ test("importing the module lands the cursor on the init-tagged row and clamps th
assert.equal(ajaxField.hidden, true);
assert.equal(extraField.textContent, "");
assert.equal(card.attributes.title, "PATCH ::not-a-url::");
assert.equal(
requestLink.attributes.href,
"/debug/view?tag=tag-3&panel=request&yii_debug_theme=dark",
);
assert.equal(
automaticLink.attributes.href,
"/debug/view?tag=tag-3&panel=auto",
);

assert.equal(newestButton.disabled, false);
assert.equal(newerButton.disabled, false);
Expand Down Expand Up @@ -296,6 +332,7 @@ test("older clicks walk toward the oldest row and refresh every snapshot facet",
assert.equal(timeField.textContent, "12:00:04");
assert.equal(timeField.hidden, false);
assert.equal(card.attributes.title, "DELETE wtf:");
assert.equal(requestLink.attributes.href.includes("tag=tag-4"), true);
assert.equal(scrolls.length, 1);

globalThis.window.innerHeight = 0;
Expand All @@ -313,6 +350,11 @@ test("older clicks walk toward the oldest row and refresh every snapshot facet",
assert.equal(timeField.hidden, true);
assert.equal(ajaxField.hidden, true);
assert.equal(card.attributes.title, "");
assert.equal(
requestLink.attributes.href.includes("tag=tag-4"),
true,
"A malformed history row must not erase the last valid Request link tag.",
);
assert.deepEqual(scrolls[1], { top: 1050, behavior: "smooth" });

globalThis.window.innerHeight = 800;
Expand Down
8 changes: 8 additions & 0 deletions src/Capture/CapturePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ public function isSensitiveKey(string $key): bool
);
}

/**
* Returns the maximum number of body bytes that may reach persistent capture.
*/
public function maxBodyBytes(): int
{
return $this->maxBodyBytes;
}

/**
* Redacts sensitive keys throughout a bounded value tree.
*
Expand Down
14 changes: 5 additions & 9 deletions src/Panel/Request/RequestSectionRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace PHPForge\Debug\Panel\Request;

use PHPForge\Debug\Helper\{Dump, Tabs, Vocabulary};
use PHPForge\Debug\Helper\{Disclosure, Dump, Tabs, Vocabulary};
use UIAwesome\Html\Flow\{Div, P};
use UIAwesome\Html\Form\InputSearch;
use UIAwesome\Html\Heading\H2;
Expand Down Expand Up @@ -79,25 +79,21 @@ public static function renderHero(RequestHero $hero): string
}

/**
* Renders a single name/value section as `<header>` + `<table>`, or as an empty-state `<p>` when the section has
* no entries.
* Renders a single name/value section as `<header>` + `<table>`, or as a collapsed disclosure when the section
* has no entries.
*/
public static function renderSection(RequestSection $section): string
{
$header = self::renderSectionHeader($section);

if ($section->entries === []) {
$emptyState = P::tag()
->class('yii-debug-table-empty')
->content('No data')
->render();

return "{$header}{$emptyState}";
return Disclosure::render($section->caption, $emptyState);
}

$table = self::renderSectionTable($section);

return "{$header}{$table}";
return self::renderSectionHeader($section) . self::renderSectionTable($section);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions tests/Capture/CapturePolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function testConstructorAcceptsTheSmallestPositiveBodyLimit(): void
'A one-byte body limit must remain valid.',
);
}

public function testConstructorRejectsANonPositiveBodyLimit(): void
{
$this->expectException(InvalidArgumentException::class);
Expand All @@ -46,6 +47,15 @@ public function testConstructorRejectsInvalidPatternConfigurationImmediately():
new CapturePolicy(sensitiveKeyPatterns: ['invalid']);
}

public function testMaxBodyBytesReturnsTheConfiguredPersistentLimit(): void
{
self::assertSame(
123,
(new CapturePolicy(maxBodyBytes: 123))->maxBodyBytes(),
'Adapters must be able to bound stream reads before applying the persistent-body policy.',
);
}

public function testPolicyDefaultPatternsAreSegmentAwareAndCanBeDisabled(): void
{
$defaultPolicy = new CapturePolicy();
Expand Down
42 changes: 22 additions & 20 deletions tests/Panel/Request/RequestSectionRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,26 +153,6 @@ public function testRenderSectionEmitsFilterInputWhenFilterableAndNonEmpty(): vo
);
}

public function testRenderSectionOmitsFilterInputWhenSectionIsEmpty(): void
{
$section = new RequestSection(caption: 'Server', entries: [], filterable: true);

self::assertSame(
<<<HTML
<header class="yii-debug-section-header">
<h2>
Server
</h2>
</header><p class="yii-debug-table-empty">
No data
</p>
HTML,
RequestSectionRenderer::renderSection($section),
'Empty section must not render the filter input.',
);

}

public function testRenderSectionPicksHtmlSpecialCharsEscapingForRowValues(): void
{
$section = new RequestSection(caption: 'Headers', entries: ['X-Custom' => "'quoted' <script>alert(1)</script>"]);
Expand Down Expand Up @@ -221,6 +201,28 @@ public function testRenderSectionRendersOneRowPerEntry(): void
);
}

public function testRenderSectionUsesCollapsedDisclosureWhenSectionIsEmpty(): void
{
$section = new RequestSection(caption: 'Server', entries: [], filterable: true);

self::assertSame(
<<<HTML
<details class="yii-debug-disclosure">
<summary class="yii-debug-disclosure-summary">
<span class="yii-debug-disclosure-title">Server</span><span class="yii-debug-disclosure-hint" aria-hidden="true"><span data-yii-debug-hint="collapsed">click to expand</span><span data-yii-debug-hint="expanded">click to collapse</span></span>
</summary><div class="yii-debug-disclosure-body">
<p class="yii-debug-table-empty">
No data
</p>
</div>
</details>
HTML,
RequestSectionRenderer::renderSection($section),
'Empty section must use the shared collapsed disclosure without a filter input.',
);

}

public function testRenderTabsMarksFirstTabActive(): void
{
$tabs = [
Expand Down
Loading