From 6074407ac00173b25adff12a859c1695b5974552 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 21:14:58 +0900 Subject: [PATCH 1/4] test(admin): cover AdminPanel (previously zero coverage) Found via a systematic component-vs-story-vs-test cross-reference of frontend/src/components/*.tsx -- AdminPanel had neither a .test.tsx nor a .stories.tsx, unlike every other component in the directory. Tests cover: save disabled until the brand name actually changes, a successful save calling updateTenantConfig (backend/app/main.py's PATCH /api/settings, now covered separately in #435) with the right arguments and reporting the new name back to the caller, and a failed save showing the error while leaving the form editable (not stuck disabled). Storybook stories cover the default state and a long brand-name layout edge case. pnpm run test: 143 passed (was 140). pnpm run lint: no new warnings. pnpm run build: the two pre-existing App.tsx errors are the unrelated main break already fixed in #426, not something this PR touches. --- .../src/components/AdminPanel.stories.tsx | 25 ++++++++++ frontend/src/components/AdminPanel.test.tsx | 48 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 frontend/src/components/AdminPanel.stories.tsx create mode 100644 frontend/src/components/AdminPanel.test.tsx diff --git a/frontend/src/components/AdminPanel.stories.tsx b/frontend/src/components/AdminPanel.stories.tsx new file mode 100644 index 000000000..f028de4b8 --- /dev/null +++ b/frontend/src/components/AdminPanel.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AdminPanel } from "./AdminPanel"; + +const meta = { + title: "Admin/AdminPanel", + component: AdminPanel, + args: { + currentBrandName: "LineageWeave", + onBrandNameChange: () => undefined, + accessToken: "demo-access-token", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +// Edge case: a long tenant brand name near the input's practical width. +export const LongBrandName: Story = { + args: { + currentBrandName: "A Very Long Tenant Brand Name For Layout Testing Purposes", + }, +}; diff --git a/frontend/src/components/AdminPanel.test.tsx b/frontend/src/components/AdminPanel.test.tsx new file mode 100644 index 000000000..3e341ab9e --- /dev/null +++ b/frontend/src/components/AdminPanel.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { AdminPanel } from "./AdminPanel"; +import * as api from "../api"; + +describe("AdminPanel", () => { + it("disables save until the brand name actually changes", () => { + render( + , + ); + expect(screen.getByRole("button", { name: "Save settings" })).toBeDisabled(); + }); + + it("saves a changed brand name and reports it back to the caller", async () => { + const updateSpy = vi + .spyOn(api, "updateTenantConfig") + .mockResolvedValue({ brandName: "Renamed Corp" }); + const onBrandNameChange = vi.fn(); + render( + , + ); + + const input = screen.getByRole("textbox", { name: "Tenant brand name" }); + await userEvent.clear(input); + await userEvent.type(input, "Renamed Corp"); + await userEvent.click(screen.getByRole("button", { name: "Save settings" })); + + expect(updateSpy).toHaveBeenCalledWith("token", "Renamed Corp"); + expect(await screen.findByText("Settings saved!")).toBeInTheDocument(); + expect(onBrandNameChange).toHaveBeenCalledWith("Renamed Corp"); + }); + + it("shows an error and leaves the form editable when the save fails", async () => { + vi.spyOn(api, "updateTenantConfig").mockRejectedValue(new Error("Failed to update settings")); + render( + , + ); + + const input = screen.getByRole("textbox", { name: "Tenant brand name" }); + await userEvent.clear(input); + await userEvent.type(input, "Renamed Corp"); + await userEvent.click(screen.getByRole("button", { name: "Save settings" })); + + expect(await screen.findByText("Failed to update settings")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Save settings" })).not.toBeDisabled(); + }); +}); From 7f6383eaf1ab51d2df478d3d54e69ac1ba003bc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 21:24:33 +0900 Subject: [PATCH 2/4] fix(frontend): use OIDC return-url helpers on the login button Same shared-ancestor bug as #418/#415/#426/#427/#429/#431/#434: the login button built an unsanitized returnUrl inline instead of returnUrlFromLocation()/rememberOidcReturnUrl(), and removed the unreachable login-screen AdminPanel render (accessToken is always undefined pre-auth). This PR's own AdminPanel.test.tsx/.stories.tsx render the component directly, so this doesn't affect its coverage. --- frontend/src/App.test.tsx | 3 +++ frontend/src/App.tsx | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..70eb27590 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -41,6 +41,9 @@ describe("App, unauthenticated", () => { state: expect.objectContaining({ returnUrl: expect.stringMatching(/^\//) }), }), ); + // Persisted as a fallback in case the OIDC state round-trip is dropped + // (see oidcReturnUrl.ts's restoreOidcReturnUrl, consumed in main.tsx). + expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toMatch(/^\//); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..1b5b351ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
- {destination === "admin" ? : null}