diff --git a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.stories.tsx b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.stories.tsx index 061b1212..6b0ce287 100644 --- a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.stories.tsx +++ b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.stories.tsx @@ -22,13 +22,11 @@ type Story = StoryObj; export const Keychain: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - await canvas.findByText("Secrets: OS keychain"), - ).toBeInTheDocument(); - await expect(canvas.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "neutral", - ); + // Asserted on the band's full text: the "Secrets:" prefix is its own + // ``, which a `findByText` string match would not see as one string. + const band = await canvas.findByTestId("secret-storage-footer"); + await expect(band).toHaveTextContent("Secrets: OS keychain"); + await expect(band).toHaveAttribute("data-tone", "neutral"); }, }; @@ -46,9 +44,8 @@ export const EncryptedFile: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - await canvas.findByText("Secrets: File (encrypted)"), - ).toBeInTheDocument(); + const band = await canvas.findByTestId("secret-storage-footer"); + await expect(band).toHaveTextContent("Secrets: File (encrypted)"); await expect( canvas.getByText("/home/node/.mcp-inspector/secrets.json"), ).toBeInTheDocument(); @@ -70,13 +67,9 @@ export const UnencryptedFile: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - await canvas.findByText("Secrets: File (unencrypted)"), - ).toBeInTheDocument(); - await expect(canvas.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "warn", - ); + const band = await canvas.findByTestId("secret-storage-footer"); + await expect(band).toHaveTextContent("Secrets: File (unencrypted)"); + await expect(band).toHaveAttribute("data-tone", "warn"); }, }; @@ -86,9 +79,8 @@ export const InMemory: Story = { args: { info: { kind: "memory", reason: "fallback", durable: false } }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - await canvas.findByText("Secrets: Memory (this session only)"), - ).toBeInTheDocument(); + const band = await canvas.findByTestId("secret-storage-footer"); + await expect(band).toHaveTextContent("Secrets: Memory (this session only)"); await expect( canvas.getByText(/lost when the Inspector exits/), ).toBeInTheDocument(); diff --git a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.test.tsx b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.test.tsx index 90dca4a2..90548f38 100644 --- a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.test.tsx +++ b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.test.tsx @@ -25,25 +25,25 @@ const memory: SecretStorageInfo = { describe("SecretStorageFooter", () => { it("names the keychain with no caveat and no warning tone", () => { renderWithMantine(); - expect(screen.getByText("Secrets: OS keychain")).toBeInTheDocument(); - expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "neutral", - ); + // Asserted on the band's full text rather than with `getByText`: the + // "Secrets:" prefix is its own ``, and testing-library matches a + // string against an element's *direct* text nodes only, so the label now + // reads as two fragments even though it renders as one line. + const band = screen.getByTestId("secret-storage-footer"); + expect(band).toHaveTextContent("Secrets: OS keychain"); + expect(band).toHaveAttribute("data-tone", "neutral"); }); it("shows the path for an encrypted file, quietly", () => { // Nothing is wrong here, so the band must not shout — the path is the // useful part and the tone stays neutral. renderWithMantine(); - expect(screen.getByText("Secrets: File (encrypted)")).toBeInTheDocument(); + const band = screen.getByTestId("secret-storage-footer"); + expect(band).toHaveTextContent("Secrets: File (encrypted)"); expect( screen.getByText("/home/node/.mcp-inspector/secrets.json"), ).toBeInTheDocument(); - expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "neutral", - ); + expect(band).toHaveAttribute("data-tone", "neutral"); }); it("renders the plaintext caveat inline, not only on hover", () => { @@ -51,28 +51,22 @@ describe("SecretStorageFooter", () => { // to hover to discover is not a warning. The caveat takes the visible // slot, displacing the path (which stays in the tooltip). renderWithMantine(); - expect(screen.getByText("Secrets: File (unencrypted)")).toBeInTheDocument(); + const band = screen.getByTestId("secret-storage-footer"); + expect(band).toHaveTextContent("Secrets: File (unencrypted)"); expect( screen.getByText(/Set MCP_INSPECTOR_SECRET_KEY to encrypt them/), ).toBeInTheDocument(); - expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "warn", - ); + expect(band).toHaveAttribute("data-tone", "warn"); }); it("warns that an in-memory store loses secrets on exit", () => { renderWithMantine(); - expect( - screen.getByText("Secrets: Memory (this session only)"), - ).toBeInTheDocument(); + const band = screen.getByTestId("secret-storage-footer"); + expect(band).toHaveTextContent("Secrets: Memory (this session only)"); expect( screen.getByText(/lost when the Inspector exits/), ).toBeInTheDocument(); - expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "warn", - ); + expect(band).toHaveAttribute("data-tone", "warn"); }); it("renders nothing when the backend didn't report a store", () => { diff --git a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.tsx b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.tsx index 8bd445ba..802e530a 100644 --- a/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.tsx +++ b/clients/web/src/components/elements/SecretStorageFooter/SecretStorageFooter.tsx @@ -47,6 +47,22 @@ const FooterLabel = Text.withProps({ variant: "nowrap", }); +// "Secrets:" carries the heavier weight so the band reads as a labelled fact +// rather than as one run of prose — the eye lands on what the line is about +// before reading which store it names. +// +// A `span` inside the label rather than a sibling in the `Group`: `Text` +// renders a `

`, so a nested one would be a `

` inside a `

` (invalid, +// and the browser would close the outer paragraph early), and a sibling would +// take the Group's `gap` and sit too far from the value it labels. `inherit` +// keeps it at the label's own size and line height instead of Mantine's `Text` +// defaults. +const FooterLabelPrefix = Text.withProps({ + span: true, + inherit: true, + fw: 700, +}); + // The detail — a path, or the caveat sentence — is the part allowed to lose // characters, since the label before it carries the answer. // @@ -128,7 +144,10 @@ export function SecretStorageFooter({ info }: SecretStorageFooterProps) { `.withProps()` rule, and it takes `color` as a prop rather than a style. */} - Secrets: {secretStorageLabel(info)} + + Secrets:{" "} + {secretStorageLabel(info)} + {detail && {detail}} ); diff --git a/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.test.tsx b/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.test.tsx index 2e02df15..097165c5 100644 --- a/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.test.tsx +++ b/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.test.tsx @@ -492,13 +492,11 @@ describe("ClientSettingsModal", () => { }} />, ); - expect( - screen.getByText("Secrets: File (unencrypted)"), - ).toBeInTheDocument(); - expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute( - "data-tone", - "warn", - ); + // Full-text assertion: the "Secrets:" prefix is its own ``, which + // `getByText`'s direct-text-node matching would not see as one string. + const band = screen.getByTestId("secret-storage-footer"); + expect(band).toHaveTextContent("Secrets: File (unencrypted)"); + expect(band).toHaveAttribute("data-tone", "warn"); }); it("renders no footer when the backend didn't report a store", () => { diff --git a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.test.tsx b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.test.tsx index 5479f22b..ba0303d4 100644 --- a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.test.tsx +++ b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.test.tsx @@ -920,9 +920,11 @@ describe("ServerSettingsModal", () => { }} />, ); - expect( - screen.getByText("Secrets: Memory (this session only)"), - ).toBeInTheDocument(); + // Full-text assertion: the "Secrets:" prefix is its own ``, which + // `getByText`'s direct-text-node matching would not see as one string. + expect(screen.getByTestId("secret-storage-footer")).toHaveTextContent( + "Secrets: Memory (this session only)", + ); }); it("renders no footer when the backend didn't report a store", () => {