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 = {
|
export const Keychain: Story = {
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
await expect(
|
// Asserted on the band's full text: the "Secrets:" prefix is its own
|
||||||
await canvas.findByText("Secrets: OS keychain"),
|
// `<span>`, which a `findByText` string match would not see as one string.
|
||||||
).toBeInTheDocument();
|
const band = await canvas.findByTestId("secret-storage-footer");
|
||||||
await expect(canvas.getByTestId("secret-storage-footer")).toHaveAttribute(
|
await expect(band).toHaveTextContent("Secrets: OS keychain");
|
||||||
"data-tone",
|
await expect(band).toHaveAttribute("data-tone", "neutral");
|
||||||
"neutral",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,9 +44,8 @@ export const EncryptedFile: Story = {
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
await expect(
|
const band = await canvas.findByTestId("secret-storage-footer");
|
||||||
await canvas.findByText("Secrets: File (encrypted)"),
|
await expect(band).toHaveTextContent("Secrets: File (encrypted)");
|
||||||
).toBeInTheDocument();
|
|
||||||
await expect(
|
await expect(
|
||||||
canvas.getByText("/home/node/.mcp-inspector/secrets.json"),
|
canvas.getByText("/home/node/.mcp-inspector/secrets.json"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
@ -70,13 +67,9 @@ export const UnencryptedFile: Story = {
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
await expect(
|
const band = await canvas.findByTestId("secret-storage-footer");
|
||||||
await canvas.findByText("Secrets: File (unencrypted)"),
|
await expect(band).toHaveTextContent("Secrets: File (unencrypted)");
|
||||||
).toBeInTheDocument();
|
await expect(band).toHaveAttribute("data-tone", "warn");
|
||||||
await expect(canvas.getByTestId("secret-storage-footer")).toHaveAttribute(
|
|
||||||
"data-tone",
|
|
||||||
"warn",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -86,9 +79,8 @@ export const InMemory: Story = {
|
||||||
args: { info: { kind: "memory", reason: "fallback", durable: false } },
|
args: { info: { kind: "memory", reason: "fallback", durable: false } },
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
await expect(
|
const band = await canvas.findByTestId("secret-storage-footer");
|
||||||
await canvas.findByText("Secrets: Memory (this session only)"),
|
await expect(band).toHaveTextContent("Secrets: Memory (this session only)");
|
||||||
).toBeInTheDocument();
|
|
||||||
await expect(
|
await expect(
|
||||||
canvas.getByText(/lost when the Inspector exits/),
|
canvas.getByText(/lost when the Inspector exits/),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -25,25 +25,25 @@ const memory: SecretStorageInfo = {
|
||||||
describe("SecretStorageFooter", () => {
|
describe("SecretStorageFooter", () => {
|
||||||
it("names the keychain with no caveat and no warning tone", () => {
|
it("names the keychain with no caveat and no warning tone", () => {
|
||||||
renderWithMantine(<SecretStorageFooter info={keyring} />);
|
renderWithMantine(<SecretStorageFooter info={keyring} />);
|
||||||
expect(screen.getByText("Secrets: OS keychain")).toBeInTheDocument();
|
// Asserted on the band's full text rather than with `getByText`: the
|
||||||
expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute(
|
// "Secrets:" prefix is its own `<span>`, and testing-library matches a
|
||||||
"data-tone",
|
// string against an element's *direct* text nodes only, so the label now
|
||||||
"neutral",
|
// 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", () => {
|
it("shows the path for an encrypted file, quietly", () => {
|
||||||
// Nothing is wrong here, so the band must not shout — the path is the
|
// Nothing is wrong here, so the band must not shout — the path is the
|
||||||
// useful part and the tone stays neutral.
|
// useful part and the tone stays neutral.
|
||||||
renderWithMantine(<SecretStorageFooter info={encryptedFile} />);
|
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(
|
expect(
|
||||||
screen.getByText("/home/node/.mcp-inspector/secrets.json"),
|
screen.getByText("/home/node/.mcp-inspector/secrets.json"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute(
|
expect(band).toHaveAttribute("data-tone", "neutral");
|
||||||
"data-tone",
|
|
||||||
"neutral",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the plaintext caveat inline, not only on hover", () => {
|
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
|
// to hover to discover is not a warning. The caveat takes the visible
|
||||||
// slot, displacing the path (which stays in the tooltip).
|
// slot, displacing the path (which stays in the tooltip).
|
||||||
renderWithMantine(<SecretStorageFooter info={plaintextFile} />);
|
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(
|
expect(
|
||||||
screen.getByText(/Set MCP_INSPECTOR_SECRET_KEY to encrypt them/),
|
screen.getByText(/Set MCP_INSPECTOR_SECRET_KEY to encrypt them/),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute(
|
expect(band).toHaveAttribute("data-tone", "warn");
|
||||||
"data-tone",
|
|
||||||
"warn",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warns that an in-memory store loses secrets on exit", () => {
|
it("warns that an in-memory store loses secrets on exit", () => {
|
||||||
renderWithMantine(<SecretStorageFooter info={memory} />);
|
renderWithMantine(<SecretStorageFooter info={memory} />);
|
||||||
expect(
|
const band = screen.getByTestId("secret-storage-footer");
|
||||||
screen.getByText("Secrets: Memory (this session only)"),
|
expect(band).toHaveTextContent("Secrets: Memory (this session only)");
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/lost when the Inspector exits/),
|
screen.getByText(/lost when the Inspector exits/),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute(
|
expect(band).toHaveAttribute("data-tone", "warn");
|
||||||
"data-tone",
|
|
||||||
"warn",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders nothing when the backend didn't report a store", () => {
|
it("renders nothing when the backend didn't report a store", () => {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,22 @@ const FooterLabel = Text.withProps({
|
||||||
variant: "nowrap",
|
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
|
// The detail — a path, or the caveat sentence — is the part allowed to lose
|
||||||
// characters, since the label before it carries the answer.
|
// 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
|
`.withProps()` rule, and it takes `color` as a prop rather than a
|
||||||
style. */}
|
style. */}
|
||||||
<Icon size={ICON_SIZE} color={color} aria-hidden />
|
<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>}
|
{detail && <FooterDetail c={color}>{detail}</FooterDetail>}
|
||||||
</FooterBand>
|
</FooterBand>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -492,13 +492,11 @@ describe("ClientSettingsModal", () => {
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(
|
// Full-text assertion: the "Secrets:" prefix is its own `<span>`, which
|
||||||
screen.getByText("Secrets: File (unencrypted)"),
|
// `getByText`'s direct-text-node matching would not see as one string.
|
||||||
).toBeInTheDocument();
|
const band = screen.getByTestId("secret-storage-footer");
|
||||||
expect(screen.getByTestId("secret-storage-footer")).toHaveAttribute(
|
expect(band).toHaveTextContent("Secrets: File (unencrypted)");
|
||||||
"data-tone",
|
expect(band).toHaveAttribute("data-tone", "warn");
|
||||||
"warn",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders no footer when the backend didn't report a store", () => {
|
it("renders no footer when the backend didn't report a store", () => {
|
||||||
|
|
|
||||||
|
|
@ -920,9 +920,11 @@ describe("ServerSettingsModal", () => {
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(
|
// Full-text assertion: the "Secrets:" prefix is its own `<span>`, which
|
||||||
screen.getByText("Secrets: Memory (this session only)"),
|
// `getByText`'s direct-text-node matching would not see as one string.
|
||||||
).toBeInTheDocument();
|
expect(screen.getByTestId("secret-storage-footer")).toHaveTextContent(
|
||||||
|
"Secrets: Memory (this session only)",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders no footer when the backend didn't report a store", () => {
|
it("renders no footer when the backend didn't report a store", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue