feat(FN-2443): unify plugin settings into tabbed Plugins section

- Remove the standalone Pi Extensions section from settings navigation and route legacy initialSection values to Plugins
- Add accessible subsection tabs in Plugins for Fusion Plugins and Pi Extensions with proper tab/tabpanel semantics
- Render plugin content conditionally per active subsection so only the selected manager is mounted
- Add SettingsModal tests covering sidebar navigation, tab accessibility, and subsection switching behavior
- Add subsection styling in SettingsModal.css and preserve extracted styles.css boundaries
This commit is contained in:
Fusion
2026-04-25 10:35:27 -07:00
committed by gsxdsm
parent 01f3e70917
commit d09ae5b8df
3 changed files with 197 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SettingsModal } from "../SettingsModal";
import type { SettingsExportData } from "../../api";
@@ -61,6 +61,18 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
useMemoryBackendStatus: (...args: unknown[]) => mockUseMemoryBackendStatus(...args),
}));
vi.mock("../PluginManager", () => ({
PluginManager: () => <div data-testid="plugin-manager">Plugin manager content</div>,
}));
vi.mock("../PiExtensionsManager", () => ({
PiExtensionsManager: () => <div data-testid="pi-extensions-manager">Pi extensions content</div>,
}));
vi.mock("../PluginSlot", () => ({
PluginSlot: () => <div data-testid="plugin-slot">Plugin slot content</div>,
}));
const noop = () => {};
const defaultSettings = {
@@ -90,6 +102,13 @@ function renderModal(props = {}) {
);
}
async function waitForSettingsModalReady() {
await waitFor(() => {
expect(mockFetchSettings).toHaveBeenCalled();
expect(screen.queryByText("Loading…")).not.toBeInTheDocument();
});
}
describe("SettingsModal", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -318,6 +337,55 @@ describe("SettingsModal", () => {
});
});
describe("Plugins section navigation", () => {
it("does not render a standalone Pi Extensions sidebar item", async () => {
renderModal();
await waitForSettingsModalReady();
const sidebar = document.querySelector(".settings-sidebar");
expect(sidebar).toBeInTheDocument();
expect(within(sidebar as HTMLElement).queryByRole("button", { name: /Pi Extensions$/ })).not.toBeInTheDocument();
expect(within(sidebar as HTMLElement).getByRole("button", { name: /Plugins$/ })).toBeInTheDocument();
});
it("renders accessible tab semantics for Plugins subsection controls", async () => {
renderModal();
await waitForSettingsModalReady();
await userEvent.click(await screen.findByRole("button", { name: /Plugins$/ }));
const tablist = await screen.findByRole("tablist", { name: "Plugin manager type" });
const fusionTab = within(tablist).getByRole("tab", { name: "Fusion Plugins" });
const piTab = within(tablist).getByRole("tab", { name: "Pi Extensions" });
expect(fusionTab).toHaveAttribute("aria-selected", "true");
expect(fusionTab).toHaveAttribute("aria-controls", "plugins-panel-fusion-plugins");
expect(piTab).toHaveAttribute("aria-selected", "false");
expect(piTab).toHaveAttribute("aria-controls", "plugins-panel-pi-extensions");
});
it("switches between Fusion Plugins and Pi Extensions managers", async () => {
renderModal();
await waitForSettingsModalReady();
await userEvent.click(await screen.findByRole("button", { name: /Plugins$/ }));
expect(await screen.findByTestId("plugin-manager")).toBeInTheDocument();
expect(screen.getByTestId("plugin-slot")).toBeInTheDocument();
expect(screen.queryByTestId("pi-extensions-manager")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("tab", { name: "Pi Extensions" }));
await waitFor(() => {
expect(screen.getByTestId("pi-extensions-manager")).toBeInTheDocument();
});
expect(screen.queryByTestId("plugin-manager")).not.toBeInTheDocument();
expect(screen.queryByTestId("plugin-slot")).not.toBeInTheDocument();
expect(screen.getByRole("tabpanel", { name: "Pi Extensions" })).toBeVisible();
expect(document.getElementById("plugins-panel-fusion-plugins")).toHaveAttribute("hidden");
});
});
describe("Number input clearing", () => {
it("allows clearing maxConcurrent without leaving a stuck zero", async () => {
renderModal();