diff --git a/CHANGELOG.md b/CHANGELOG.md index 2575aa8..088933e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/Helper/Tabs.php b/src/Helper/Tabs.php index b239831..508f17f 100644 --- a/src/Helper/Tabs.php +++ b/src/Helper/Tabs.php @@ -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. */ @@ -17,14 +20,22 @@ final class Tabs * @param non-empty-string $id * @param non-empty-string $ariaLabel * @param list $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; + $tabId = "{$id}-tab-{$index}"; $panelId = "{$id}-panel-{$index}"; diff --git a/src/Panel/User/UserGuestRenderer.php b/src/Panel/User/UserGuestRenderer.php new file mode 100644 index 0000000..eb5875d --- /dev/null +++ b/src/Panel/User/UserGuestRenderer.php @@ -0,0 +1,32 @@ +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.', + ), + ); + } +} diff --git a/src/Panel/User/UserRbacRenderer.php b/src/Panel/User/UserRbacRenderer.php new file mode 100644 index 0000000..81430dc --- /dev/null +++ b/src/Panel/User/UserRbacRenderer.php @@ -0,0 +1,41 @@ +content('Roles') + ->render() . $rolesGrid; + } + + if ($permissionsGrid !== null) { + $html .= H2::tag() + ->content('Permissions') + ->render() . $permissionsGrid; + } + + return $html; + } +} diff --git a/tests/Helper/TabsTest.php b/tests/Helper/TabsTest.php index 2b5b93d..7fb96f1 100644 --- a/tests/Helper/TabsTest.php +++ b/tests/Helper/TabsTest.php @@ -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; @@ -16,6 +17,19 @@ #[Group('helpers')] final class TabsTest extends TestCase { + public function testRenderAllowsAnyActiveIndexForAnEmptyTabList(): void + { + self::assertSame( + << +
+
+ 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( @@ -52,4 +66,61 @@ public function testRenderMarksOnlyTheFirstTabAndPanelAsActive(): void 'Inactive panel must remain hidden until its tab is selected.', ); } + + public function testRenderSelectsTheRequestedInitialTab(): void + { + self::assertSame( + << + +
+
+

Two

+
+
+ HTML, + Tabs::render( + 'example', + 'Example tabs', + [ + ['label' => 'First', 'content' => '

One

'], + ['label' => 'Second', 'content' => '

Two

'], + ], + 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' => '

One

']], + -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' => '

One

']], + 1, + ); + } } diff --git a/tests/Panel/User/UserGuestRendererTest.php b/tests/Panel/User/UserGuestRendererTest.php new file mode 100644 index 0000000..e96f885 --- /dev/null +++ b/tests/Panel/User/UserGuestRendererTest.php @@ -0,0 +1,36 @@ + +

+ No user authenticated in this request +

+ The request was served to a guest, so there are no identity attributes, roles, or permissions to inspect. +

+ Sign in and reload to inspect the identity. User switching remains unavailable to guests. +

+ + HTML, + UserGuestRenderer::render(), + 'Guest rendering must remain identical across framework adapters.', + ); + } +} diff --git a/tests/Panel/User/UserRbacRendererTest.php b/tests/Panel/User/UserRbacRendererTest.php new file mode 100644 index 0000000..5500c7b --- /dev/null +++ b/tests/Panel/User/UserRbacRendererTest.php @@ -0,0 +1,47 @@ + + Permissions + + HTML, + UserRbacRenderer::render(null, ''), + 'A null category must be omitted while a captured empty category keeps its heading.', + ); + } + public function testRenderReturnsBothSectionsInCanonicalOrder(): void + { + self::assertSame( + << + Roles +

+ Permissions +

+ HTML, + UserRbacRenderer::render( + '
', + '
', + ), + 'Roles must precede Permissions with the canonical headings.', + ); + } +}