feat(FN-5161): restore SecretsView eye toggle icon styling

Restores toggle icon styling in `SecretsView.css` and adds test coverage for the eye-toggle reveal/hide behavior in `SecretsView`, completing the FN-5161 test suite for the secrets view.

Fusion-Task-Id: FN-5161
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 23:35:11 -07:00
committed by gsxdsm
parent 61ef09c79c
commit 0191bef404
3 changed files with 72 additions and 3 deletions

View File

@@ -160,6 +160,15 @@
flex: 1; flex: 1;
} }
.secrets-visibility-toggle {
color: var(--text);
}
.secrets-visibility-toggle > svg {
display: block;
stroke: currentColor;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.secrets-view { .secrets-view {
padding-inline: var(--space-md); padding-inline: var(--space-md);

View File

@@ -301,8 +301,13 @@ export const SecretsView = ({ addToast }: SecretsViewProps) => {
<div className="secrets-row-side"> <div className="secrets-row-side">
<span className="secrets-row-read">{secret.lastReadAt ? new Date(secret.lastReadAt).toLocaleString() : "Never read"}</span> <span className="secrets-row-read">{secret.lastReadAt ? new Date(secret.lastReadAt).toLocaleString() : "Never read"}</span>
<div className="secrets-row-actions"> <div className="secrets-row-actions">
<button className="btn btn-icon" onClick={() => void revealSecret(secret)} aria-label="Reveal"> <button
{revealed ? <EyeOff size={14} /> : <Eye size={14} />} type="button"
className="btn btn-icon secrets-visibility-toggle"
onClick={() => void revealSecret(secret)}
aria-label={revealed ? "Hide" : "Reveal"}
>
{revealed ? <EyeOff size={14} aria-hidden="true" /> : <Eye size={14} aria-hidden="true" />}
</button> </button>
<button className="btn btn-icon" onClick={() => void copySecret(secret)} aria-label="Copy" disabled={!revealed}> <button className="btn btn-icon" onClick={() => void copySecret(secret)} aria-label="Copy" disabled={!revealed}>
{copiedId === secret.id ? <Check size={14} /> : <Copy size={14} />} {copiedId === secret.id ? <Check size={14} /> : <Copy size={14} />}
@@ -348,7 +353,7 @@ export const SecretsView = ({ addToast }: SecretsViewProps) => {
</div> </div>
<div className="secrets-modal-body"> <div className="secrets-modal-body">
<div className="form-group"><label>Key</label><input className="input" value={form.key} onChange={(e) => setForm((c) => ({ ...c, key: e.target.value }))} /></div> <div className="form-group"><label>Key</label><input className="input" value={form.key} onChange={(e) => setForm((c) => ({ ...c, key: e.target.value }))} /></div>
<div className="form-group"><label>Value</label><div className="secrets-value-row"><input className="input" type={showValue ? "text" : "password"} autoComplete="off" spellCheck={false} value={form.value} onChange={(e) => setForm((c) => ({ ...c, value: e.target.value }))} /><button className="btn btn-icon" onClick={() => setShowValue((s) => !s)}>{showValue ? <EyeOff size={14} /> : <Eye size={14} />}</button></div></div> <div className="form-group"><label>Value</label><div className="secrets-value-row"><input className="input" type={showValue ? "text" : "password"} autoComplete="off" spellCheck={false} value={form.value} onChange={(e) => setForm((c) => ({ ...c, value: e.target.value }))} /><button type="button" className="btn btn-icon secrets-visibility-toggle" onClick={() => setShowValue((s) => !s)} aria-label={showValue ? "Hide value" : "Show value"}>{showValue ? <EyeOff size={14} aria-hidden="true" /> : <Eye size={14} aria-hidden="true" />}</button></div></div>
<div className="form-group"><label>Description</label><textarea className="input" value={form.description} onChange={(e) => setForm((c) => ({ ...c, description: e.target.value }))} /></div> <div className="form-group"><label>Description</label><textarea className="input" value={form.description} onChange={(e) => setForm((c) => ({ ...c, description: e.target.value }))} /></div>
<div className="form-group"><label>Scope</label><div className="secrets-radio-row"><label><input type="radio" checked={form.scope === "project"} onChange={() => setForm((c) => ({ ...c, scope: "project" }))} disabled={Boolean(editing)} /> Project</label><label><input type="radio" checked={form.scope === "global"} onChange={() => setForm((c) => ({ ...c, scope: "global" }))} disabled={Boolean(editing)} /> Global</label></div></div> <div className="form-group"><label>Scope</label><div className="secrets-radio-row"><label><input type="radio" checked={form.scope === "project"} onChange={() => setForm((c) => ({ ...c, scope: "project" }))} disabled={Boolean(editing)} /> Project</label><label><input type="radio" checked={form.scope === "global"} onChange={() => setForm((c) => ({ ...c, scope: "global" }))} disabled={Boolean(editing)} /> Global</label></div></div>
<div className="form-group"><label>Access policy</label><select className="select" value={form.accessPolicy} onChange={(e) => setForm((c) => ({ ...c, accessPolicy: e.target.value as SecretPolicy }))}><option value="auto">auto</option><option value="prompt">prompt</option><option value="deny">deny</option></select></div> <div className="form-group"><label>Access policy</label><select className="select" value={form.accessPolicy} onChange={(e) => setForm((c) => ({ ...c, accessPolicy: e.target.value as SecretPolicy }))}><option value="auto">auto</option><option value="prompt">prompt</option><option value="deny">deny</option></select></div>

View File

@@ -17,6 +17,15 @@ function mockJsonResponse({ ok, status = ok ? 200 : 400, body }: JsonResponse):
} as unknown as Response; } as unknown as Response;
} }
function expectVisibleEyeIcon(svg: SVGElement) {
const style = getComputedStyle(svg);
expect(svg).toBeInTheDocument();
expect(svg.getAttribute("width")).toBeTruthy();
expect(svg.getAttribute("height")).toBeTruthy();
expect(style.display).toBe("block");
expect(style.stroke).not.toBe("none");
}
describe("SecretsView", () => { describe("SecretsView", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -145,4 +154,50 @@ describe("SecretsView", () => {
expect(await screen.findByText("VISIBLE")).toBeInTheDocument(); expect(await screen.findByText("VISIBLE")).toBeInTheDocument();
expect(screen.queryByText("__sync_passphrase__")).not.toBeInTheDocument(); expect(screen.queryByText("__sync_passphrase__")).not.toBeInTheDocument();
}); });
it("renders a visible row reveal icon", async () => {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValueOnce(
mockJsonResponse({
ok: true,
body: {
secrets: [
{ id: "secret-1", key: "VISIBLE", scope: "project", description: null, accessPolicy: "prompt", envExportable: false, envExportKey: null, lastReadAt: null },
],
},
}),
)
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { configured: false } })),
);
render(<SecretsView addToast={vi.fn()} />);
const revealButton = await screen.findByRole("button", { name: "Reveal" });
const svg = revealButton.querySelector("svg");
expect(svg).not.toBeNull();
expectVisibleEyeIcon(svg as SVGElement);
});
it("renders a visible modal value toggle icon", async () => {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { secrets: [] } }))
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { configured: false } })),
);
render(<SecretsView addToast={vi.fn()} />);
await screen.findByText("Not configured");
await userEvent.click(screen.getByRole("button", { name: "Add Secret" }));
const toggleButton = screen.getByRole("button", { name: "Show value" });
const svg = toggleButton.querySelector("svg");
expect(svg).not.toBeNull();
expectVisibleEyeIcon(svg as SVGElement);
});
}); });