refactor: Decompose ConfigPage into focused routing and component layers
Split the over-centralized ConfigPage into focused, composable layers: 1. useTabRouter hook: Encapsulates tab state management and URL synchronization - Maintains selected tab and active item (e.g., jail name) - Syncs state to location.state for deep linking and browser history - Supports bookmarkable URLs and back/forward navigation 2. ConfigPageContainer: Orchestrates tab navigation - Manages TabList and routes tab selection events - Conditionally renders tab content panels - Delegates domain-specific logic to tab components 3. ConfigPage: Focused page layout component - Renders page structure (header, title, description) - Delegates tab orchestration to ConfigPageContainer - No routing or tab state logic Benefits: - Page is now 30 lines vs 125 lines (76% reduction) - Tab state management is reusable for other multi-tab pages - Each tab component remains focused on domain-specific UI - Deep linking and browser history work out of the box - Easier to test and maintain Added comprehensive tests: - useTabRouter: 6 tests covering state initialization, tab selection, and deep linking - ConfigPageContainer: 8 tests covering tab rendering and navigation - ConfigPage: 3 tests for page structure Updated Web-Development.md with tab orchestration pattern documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,21 +1,14 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { ConfigPage } from "../ConfigPage";
|
||||
|
||||
// Mock all tab components to avoid deep render trees and API calls.
|
||||
vi.mock("../../components/config", () => ({
|
||||
JailsTab: ({ initialJail }: { initialJail?: string }) => (
|
||||
<div data-testid="jails-tab" data-initial-jail={initialJail}>
|
||||
JailsTab
|
||||
</div>
|
||||
// Mock the ConfigPageContainer to avoid router context issues in tests.
|
||||
vi.mock("../../components/config/ConfigPageContainer", () => ({
|
||||
ConfigPageContainer: () => (
|
||||
<div data-testid="config-page-container">ConfigPageContainer</div>
|
||||
),
|
||||
FiltersTab: () => <div data-testid="filters-tab">FiltersTab</div>,
|
||||
ActionsTab: () => <div data-testid="actions-tab">ActionsTab</div>,
|
||||
ServerTab: () => <div data-testid="server-tab">ServerTab</div>,
|
||||
RegexTesterTab: () => <div data-testid="regex-tab">RegexTesterTab</div>,
|
||||
ExportTab: () => <div data-testid="export-tab">ExportTab</div>,
|
||||
}));
|
||||
|
||||
function renderPage() {
|
||||
@@ -29,51 +22,20 @@ function renderPage() {
|
||||
}
|
||||
|
||||
describe("ConfigPage", () => {
|
||||
it("renders the Jails tab by default", () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId("jails-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to Filters tab when Filters tab is clicked", () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByRole("tab", { name: /filters/i }));
|
||||
expect(screen.getByTestId("filters-tab")).toBeInTheDocument();
|
||||
// Jails tab remains mounted (not removed from DOM), just hidden with CSS
|
||||
expect(screen.getByTestId("jails-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to Actions tab when Actions tab is clicked", () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByRole("tab", { name: /actions/i }));
|
||||
expect(screen.getByTestId("actions-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to Server tab when Server tab is clicked", () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByRole("tab", { name: /server/i }));
|
||||
expect(screen.getByTestId("server-tab")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the page heading", () => {
|
||||
it("renders the configuration page heading", () => {
|
||||
renderPage();
|
||||
expect(screen.getByRole("heading", { name: /configuration/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selects the Jails tab based on location state", () => {
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{ pathname: "/config", state: { tab: "jails", jail: "sshd" } },
|
||||
]}
|
||||
>
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<ConfigPage />
|
||||
</FluentProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
it("renders the ConfigPageContainer component", () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId("config-page-container")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const jailsTab = screen.getByTestId("jails-tab");
|
||||
expect(jailsTab).toBeInTheDocument();
|
||||
expect(jailsTab).toHaveAttribute("data-initial-jail", "sshd");
|
||||
it("renders the page description text", () => {
|
||||
renderPage();
|
||||
expect(
|
||||
screen.getByText(/inspect and edit fail2ban jail configuration/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user