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")