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 @@ -20,3 +20,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- feat(ui): share database EXPLAIN markup across debugger adapters.
- feat(ui): add shared profiler normalization and Timeline rendering contracts.
- feat(tests): add unit tests for panel snapshots and enhance existing test coverage.
- feat(ui): share User guest and RBAC section rendering and support selecting filtered tabs.
15 changes: 13 additions & 2 deletions src/Helper/Tabs.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace PHPForge\Debug\Helper;

use InvalidArgumentException;
use UIAwesome\Html\Flow\Div;
use UIAwesome\Html\List\{Li, Ul};
use UIAwesome\Html\Palpable\A;

use function array_key_exists;

/**
* Renders the shared accessible tab pattern used by debug panels.
*/
Expand All @@ -17,14 +20,22 @@ final class Tabs
* @param non-empty-string $id
* @param non-empty-string $ariaLabel
* @param list<array{label: string, content: string}> $tabs
* @param int $activeIndex Zero-based index of the tab selected on initial render.
*/
public static function render(string $id, string $ariaLabel, array $tabs): string
public static function render(string $id, string $ariaLabel, array $tabs, int $activeIndex = 0): string
{
if ($tabs !== [] && !array_key_exists($activeIndex, $tabs)) {
throw new InvalidArgumentException(
'The active tab index must identify a supplied tab.',
);
}

$items = [];
$panels = [];

foreach ($tabs as $index => $tab) {
$active = $index === 0;
$active = $index === $activeIndex;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$tabId = "{$id}-tab-{$index}";
$panelId = "{$id}-panel-{$index}";

Expand Down
32 changes: 32 additions & 0 deletions src/Panel/User/UserGuestRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Panel\User;

use PHPForge\Debug\Helper\EmptyState;
use UIAwesome\Html\Flow\P;

/**
* Renders the framework-neutral Guest state shared by User panels.
*/
final class UserGuestRenderer
{
/**
* Renders the complete Guest empty-state card.
*/
public static function render(): string
{
return EmptyState::card(
'No user authenticated in this request',
P::tag()
->content(
'The request was served to a guest, so there are no identity attributes, roles, or permissions to inspect.',
),
P::tag()
->content(
'Sign in and reload to inspect the identity. User switching remains unavailable to guests.',
),
);
}
}
41 changes: 41 additions & 0 deletions src/Panel/User/UserRbacRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Panel\User;

use UIAwesome\Html\Heading\H2;

/**
* Composes the shared Roles and Permissions section around adapter-owned grid markup.
*/
final class UserRbacRenderer
{
/**
* Renders role and permission grids in their canonical order.
*
* A `null` grid means that RBAC category was not captured and omits its heading. An empty string means the category
* was captured with no rows and keeps the heading so the adapter grid may expose its native empty result state.
*
* @param string|null $rolesGrid Trusted adapter-rendered Roles grid markup.
* @param string|null $permissionsGrid Trusted adapter-rendered Permissions grid markup.
*/
public static function render(string|null $rolesGrid, string|null $permissionsGrid): string
{
$html = '';

if ($rolesGrid !== null) {
$html .= H2::tag()
->content('Roles')
->render() . $rolesGrid;
}

if ($permissionsGrid !== null) {
$html .= H2::tag()
->content('Permissions')
->render() . $permissionsGrid;
}

return $html;
}
}
71 changes: 71 additions & 0 deletions tests/Helper/TabsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PHPForge\Debug\Tests\Helper;

use InvalidArgumentException;
use PHPForge\Debug\Helper\Tabs;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
Expand All @@ -16,6 +17,19 @@
#[Group('helpers')]
final class TabsTest extends TestCase
{
public function testRenderAllowsAnyActiveIndexForAnEmptyTabList(): void
{
self::assertSame(
<<<HTML
<ul class="yii-debug-tabs" role="tablist" aria-label="Empty tabs">
</ul><div class="yii-debug-tab-content">
</div>
HTML,
Tabs::render('empty', 'Empty tabs', [], -1),
'An empty tab list must preserve its existing rendering for any requested index.',
);
}

public function testRenderMarksOnlyTheFirstTabAndPanelAsActive(): void
{
$html = Tabs::render(
Expand Down Expand Up @@ -52,4 +66,61 @@ public function testRenderMarksOnlyTheFirstTabAndPanelAsActive(): void
'Inactive panel must remain hidden until its tab is selected.',
);
}

public function testRenderSelectsTheRequestedInitialTab(): void
{
self::assertSame(
<<<HTML
<ul class="yii-debug-tabs" role="tablist" aria-label="Example tabs">
<li class="yii-debug-tab" role="presentation">
<a class="yii-debug-tab-link" id="example-tab-0" href="#example-panel-0" role="tab" tabindex="-1" aria-controls="example-panel-0" aria-selected="false" data-yii-debug-toggle="tab">First</a>
</li><li class="yii-debug-tab" role="presentation">
<a class="yii-debug-tab-link is-active" id="example-tab-1" href="#example-panel-1" role="tab" tabindex="0" aria-controls="example-panel-1" aria-selected="true" data-yii-debug-toggle="tab">Second</a>
</li>
</ul><div class="yii-debug-tab-content">
<div class="yii-debug-tab-panel" id="example-panel-0" role="tabpanel" aria-labelledby="example-tab-0" hidden>
<p>One</p>
</div><div class="yii-debug-tab-panel is-active" id="example-panel-1" role="tabpanel" aria-labelledby="example-tab-1">
<p>Two</p>
</div>
</div>
HTML,
Tabs::render(
'example',
'Example tabs',
[
['label' => 'First', 'content' => '<p>One</p>'],
['label' => 'Second', 'content' => '<p>Two</p>'],
],
1,
),
'The requested tab and panel must be the only initially active pair.',
);
}

public function testRenderThrowsForANegativeActiveIndex(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The active tab index must identify a supplied tab.');

Tabs::render(
'example',
'Example tabs',
[['label' => 'First', 'content' => '<p>One</p>']],
-1,
);
}

public function testRenderThrowsForAnOutOfRangeActiveIndex(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The active tab index must identify a supplied tab.');

Tabs::render(
'example',
'Example tabs',
[['label' => 'First', 'content' => '<p>One</p>']],
1,
);
}
}
36 changes: 36 additions & 0 deletions tests/Panel/User/UserGuestRendererTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Tests\Panel\User;

use PHPForge\Debug\Panel\User\UserGuestRenderer;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

/**
* Unit tests for {@see UserGuestRenderer} freezing the cross-adapter Guest semantics and exact empty-state markup.
*/
#[Group('panel')]
#[Group('user')]
final class UserGuestRendererTest extends TestCase
{
public function testRenderReturnsTheCompleteGuestState(): void
{
self::assertSame(
<<<HTML
<div class="yii-debug-empty-state">
<h2>
No user authenticated in this request
</h2><p>
The request was served to a guest, so there are no identity attributes, roles, or permissions to inspect.
</p><p>
Sign in and reload to inspect the identity. User switching remains unavailable to guests.
</p>
</div>
HTML,
UserGuestRenderer::render(),
'Guest rendering must remain identical across framework adapters.',
);
}
}
47 changes: 47 additions & 0 deletions tests/Panel/User/UserRbacRendererTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Tests\Panel\User;

use PHPForge\Debug\Panel\User\UserRbacRenderer;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

/**
* Unit tests for {@see UserRbacRenderer} freezing RBAC section ordering and missing-category behavior.
*/
#[Group('panel')]
#[Group('user')]
final class UserRbacRendererTest extends TestCase
{
public function testRenderOmitsOnlyUncapturedSections(): void
{
self::assertSame(
<<<HTML
<h2>
Permissions
</h2>
HTML,
UserRbacRenderer::render(null, ''),
'A null category must be omitted while a captured empty category keeps its heading.',
);
}
public function testRenderReturnsBothSectionsInCanonicalOrder(): void
{
self::assertSame(
<<<HTML
<h2>
Roles
</h2><table id="roles"></table><h2>
Permissions
</h2><table id="permissions"></table>
HTML,
UserRbacRenderer::render(
'<table id="roles"></table>',
'<table id="permissions"></table>',
),
'Roles must precede Permissions with the canonical headings.',
);
}
}
Loading