style(web): bold the "Secrets:" prefix in the settings-modal footers
Weights the label so the band reads as a labelled fact rather than one run of prose — the eye lands on what the line is about before reading which store it names. Rendered as a `span` inside the existing `Text` rather than as a sibling in the `Group`: `Text` renders a `<p>`, so a nested one would be a `<p>` inside a `<p>` (invalid, and the browser closes 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. Splitting the text is why the assertions move off `getByText`: it matches a string against an element's *direct* text nodes, so "Secrets: OS keychain" now reads as two fragments even though it renders as one line. The unit tests, both modal tests, and the four stories assert `toHaveTextContent` on the band instead, which reads the full subtree. Verified in the running dev server: prefix 700, store name 500, both at the same 12px. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
b143dd6cbd
commit
6460d1804a
5 changed files with 58 additions and 53 deletions
|
|
@ -22,13 +22,11 @@ type Story = StoryObj<typeof SecretStorageFooter>;
|
|||
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
|
||||
// `<span>`, 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();
|
||||
|
|
|
|||
|
|
@ -25,25 +25,25 @@ const memory: SecretStorageInfo = {
|
|||
describe("SecretStorageFooter", () => {
|
||||
it("names the keychain with no caveat and no warning tone", () => {
|
||||
renderWithMantine(<SecretStorageFooter info={keyring} />);
|
||||
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 `<span>`, 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(<SecretStorageFooter info={encryptedFile} />);
|
||||
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(<SecretStorageFooter info={plaintextFile} />);
|
||||
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(<SecretStorageFooter info={memory} />);
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -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 `<p>`, so a nested one would be a `<p>` inside a `<p>` (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. */}
|
||||
<Icon size={ICON_SIZE} color={color} aria-hidden />
|
||||
<FooterLabel c={color}>Secrets: {secretStorageLabel(info)}</FooterLabel>
|
||||
<FooterLabel c={color}>
|
||||
<FooterLabelPrefix>Secrets:</FooterLabelPrefix>{" "}
|
||||
{secretStorageLabel(info)}
|
||||
</FooterLabel>
|
||||
{detail && <FooterDetail c={color}>{detail}</FooterDetail>}
|
||||
</FooterBand>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 `<span>`, 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", () => {
|
||||
|
|
|
|||
|
|
@ -920,9 +920,11 @@ describe("ServerSettingsModal", () => {
|
|||
}}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("Secrets: Memory (this session only)"),
|
||||
).toBeInTheDocument();
|
||||
// Full-text assertion: the "Secrets:" prefix is its own `<span>`, 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", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue