diff --git a/src/components/layout/tool-favorite-control.tsx b/src/components/layout/tool-favorite-control.tsx index e2bac980..9bbbcd40 100644 --- a/src/components/layout/tool-favorite-control.tsx +++ b/src/components/layout/tool-favorite-control.tsx @@ -32,13 +32,19 @@ export function ToolFavoriteControl({ toolKey }: ToolFavoriteControlProps) { }, []) return ( -
+
) } - diff --git a/src/features/tools/log-scrubber/page.tsx b/src/features/tools/log-scrubber/page.tsx index 5f96b479..996f80da 100644 --- a/src/features/tools/log-scrubber/page.tsx +++ b/src/features/tools/log-scrubber/page.tsx @@ -1,15 +1,14 @@ "use client" import * as React from "react" -import Link from "next/link" -import { Copy, FileText, ShieldCheck, Workflow } from "lucide-react" +import { Copy, Eraser, FileText, ShieldCheck, Workflow } from "lucide-react" import { toast } from "sonner" -import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { useLang } from "@/core/i18n/lang-provider" import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning" +import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" import { safeClipboardWrite } from "@/core/clipboard/clipboard" import { buildSensitiveToolHandoffLink } from "@/core/routing/tool-handoff" import { @@ -89,6 +88,55 @@ password=hunter2`) setFindings([]) }, []) + const handleClear = React.useCallback(() => { + setInput("") + setOutput("") + setFindings([]) + setOptions(DEFAULT_SCRUB_OPTIONS) + }, []) + + const actions: ToolAction[] = [ + { + id: "sample", + label: t.common.sample, + icon: FileText, + onClick: loadExample, + }, + { + id: "clear", + label: t.common.clear, + icon: Eraser, + onClick: handleClear, + destructive: true, + }, + { + id: "run", + label: text("scrub_action"), + icon: ShieldCheck, + onClick: runScrub, + variant: "default", + disabled: !input.trim(), + disabledReason: t.common.action_disabled_input_required, + }, + { + id: "copy", + label: t.common.copy, + icon: Copy, + onClick: () => void copyOutput(), + disabled: !output, + disabledReason: t.common.action_disabled_no_output, + }, + { + id: "to_pipeline_builder", + label: (t.tools["pipeline_builder"] as Record | undefined)?.title ?? "Pipeline Builder", + icon: Workflow, + href: pipelineHandoff.href, + onClick: pipelineHandoff.prime, + disabled: !handoffPayload.trim(), + disabledReason: t.common.action_disabled_no_output, + }, + ] + return (
@@ -99,37 +147,7 @@ password=hunter2`)

{text("description")}

-
- - - - -
+
diff --git a/tests/component/http-request-builder-page.test.tsx b/tests/component/http-request-builder-page.test.tsx index 1cbfc22c..bcebaf08 100644 --- a/tests/component/http-request-builder-page.test.tsx +++ b/tests/component/http-request-builder-page.test.tsx @@ -2,9 +2,15 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { LangProvider } from "@/core/i18n/lang-provider" import { getTranslation } from "@/core/i18n/translations/catalog" +import { RoutePageChrome } from "@/components/layout/route-page-chrome" import { HttpRequestBuilderPage } from "@/features/tools/http-request-builder/page" const clipboardWriteMock = vi.fn() +const readFavoriteToolKeysMock = vi.fn(() => []) +const toggleFavoriteToolKeyMock = vi.fn((toolKey: string) => { + void toolKey + return ["http_request_builder"] +}) vi.mock("next/navigation", () => ({ usePathname: () => "/en/http-request-builder", @@ -14,6 +20,15 @@ vi.mock("@/core/clipboard/clipboard", () => ({ safeClipboardWrite: (value: string) => clipboardWriteMock(value), })) +vi.mock("@/core/storage/tool-discovery-state", async (importOriginal) => { + const actual = await importOriginal() as typeof import("@/core/storage/tool-discovery-state") + return { + ...actual, + readFavoriteToolKeys: () => readFavoriteToolKeysMock(), + toggleFavoriteToolKey: (toolKey: string) => toggleFavoriteToolKeyMock(toolKey), + } +}) + vi.mock("sonner", () => ({ toast: { error: vi.fn(), @@ -29,10 +44,21 @@ function renderHttpBuilder() { ) } +function renderHttpBuilderWithChrome() { + return render( + + + + + , + ) +} + describe("HttpRequestBuilderPage", () => { beforeEach(() => { vi.clearAllMocks() clipboardWriteMock.mockResolvedValue({ ok: true }) + readFavoriteToolKeysMock.mockReturnValue([]) }) it("generates request code without sending a network request", () => { @@ -94,4 +120,22 @@ describe("HttpRequestBuilderPage", () => { fireEvent.click(screen.getAllByRole("button", { name: /Copy/ })[1]) await waitFor(() => expect(clipboardWriteMock).not.toHaveBeenCalled()) }) + + it("keeps favorite toggles separated from structured header entry", () => { + renderHttpBuilderWithChrome() + + expect(screen.getByRole("group", { name: "Add to favorites / Remove from favorites" })).toHaveAttribute("data-tool-global-actions") + expect(screen.getByRole("button", { name: "Add to favorites" })).toBeInTheDocument() + expect(screen.getByRole("toolbar", { name: "Tool actions" })).toBeInTheDocument() + + fireEvent.click(screen.getByRole("button", { name: "Add Header" })) + fireEvent.change(screen.getAllByRole("textbox", { name: "Header name" })[1], { + target: { value: "Authorization" }, + }) + fireEvent.change(screen.getAllByRole("textbox", { name: "Value" })[1], { + target: { value: "Bearer TOKEN_PLACEHOLDER" }, + }) + + expect(toggleFavoriteToolKeyMock).not.toHaveBeenCalled() + }) }) diff --git a/tests/guards/phase1-developer-utilities-pages.test.tsx b/tests/guards/phase1-developer-utilities-pages.test.tsx index 4361894e..f336df33 100644 --- a/tests/guards/phase1-developer-utilities-pages.test.tsx +++ b/tests/guards/phase1-developer-utilities-pages.test.tsx @@ -25,7 +25,7 @@ describe("phase 1 developer utility pages", () => { expect(screen.getByRole("heading", { name: "Log Scrubber" })).toBeInTheDocument() expect(screen.getByPlaceholderText("Paste logs, stack traces, env dumps, or config snippets that may contain secrets...")).toBeInTheDocument() - expect(screen.getByRole("button", { name: /Try example/i })).toBeInTheDocument() + expect(screen.getByRole("button", { name: /Sample/i })).toBeInTheDocument() expect(screen.getByRole("button", { name: /Scrub log/i })).toBeInTheDocument() }) diff --git a/tests/guards/tool-action-consistency-guard.test.ts b/tests/guards/tool-action-consistency-guard.test.ts index de163a20..c08f43a9 100644 --- a/tests/guards/tool-action-consistency-guard.test.ts +++ b/tests/guards/tool-action-consistency-guard.test.ts @@ -28,6 +28,8 @@ describe("tool action consistency guard", () => { "src/features/tools/uuid-generator/page.tsx", "src/features/tools/youtube-thumbnail-grabber/page.tsx", "src/features/tools/pipeline-builder/page.tsx", + "src/features/tools/http-request-builder/page.tsx", + "src/features/tools/log-scrubber/page.tsx", ] for (const file of auditedFiles) { @@ -46,6 +48,8 @@ describe("tool action consistency guard", () => { "src/features/tools/uuid-generator/page.tsx", "src/features/tools/youtube-thumbnail-grabber/page.tsx", "src/features/tools/pipeline-builder/page.tsx", + "src/features/tools/http-request-builder/page.tsx", + "src/features/tools/log-scrubber/page.tsx", ] for (const file of auditedFiles) { @@ -123,6 +127,8 @@ describe("tool action consistency guard", () => { "src/features/tools/image-resizer/page.tsx", "src/features/tools/uuid-generator/page.tsx", "src/features/tools/youtube-thumbnail-grabber/page.tsx", + "src/features/tools/http-request-builder/page.tsx", + "src/features/tools/log-scrubber/page.tsx", ] for (const file of auditedFiles) { @@ -135,6 +141,9 @@ describe("tool action consistency guard", () => { "src/features/tools/base64-encode-decode/page.tsx", "src/features/tools/image-resizer/page.tsx", "src/features/tools/youtube-thumbnail-grabber/page.tsx", + "src/features/tools/jwt-decoder/page.tsx", + "src/features/tools/http-request-builder/page.tsx", + "src/features/tools/log-scrubber/page.tsx", ] for (const file of destructiveFiles) { expect(read(file), file).toContain("destructive: true")