Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/layout/tool-favorite-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ export function ToolFavoriteControl({ toolKey }: ToolFavoriteControlProps) {
}, [])

return (
<div className="mb-4 flex flex-wrap items-center justify-end gap-2">
<div
className="mb-5 flex flex-wrap items-center justify-end gap-2 border-b border-border/70 pb-4"
role="group"
aria-label={`${addLabel} / ${removeLabel}`}
data-tool-global-actions
>
<Button
type="button"
variant="outline"
size="sm"
className="min-h-10"
className="min-h-10 border-border bg-background shadow-xs"
aria-pressed={isFavorite}
title={isFavorite ? removeLabel : addLabel}
onClick={() => setFavoriteToolKeys(toggleFavoriteToolKey(toolKey))}
>
<Heart className={cn("h-4 w-4", isFavorite ? "fill-primary text-primary" : "")} />
Expand All @@ -48,4 +54,3 @@ export function ToolFavoriteControl({ toolKey }: ToolFavoriteControlProps) {
</div>
)
}

86 changes: 52 additions & 34 deletions src/features/tools/log-scrubber/page.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string, string> | undefined)?.title ?? "Pipeline Builder",
icon: Workflow,
href: pipelineHandoff.href,
onClick: pipelineHandoff.prime,
disabled: !handoffPayload.trim(),
disabledReason: t.common.action_disabled_no_output,
},
]

return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-8">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
Expand All @@ -99,37 +147,7 @@ password=hunter2`)
</h1>
<p className="mt-1 text-muted-foreground">{text("description")}</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={loadExample}>
<FileText className="mr-2 h-4 w-4" />
{t.common.try_example}
</Button>
<Button size="sm" onClick={runScrub}>
<ShieldCheck className="mr-2 h-4 w-4" />
{text("scrub_action")}
</Button>
<Button variant="outline" size="sm" onClick={() => void copyOutput()} disabled={!output}>
<Copy className="mr-2 h-4 w-4" />
{t.common.copy}
</Button>
<Button variant="outline" size="sm" asChild>
<Link
href={pipelineHandoff.href}
onClick={(event) => {
if (!handoffPayload.trim()) {
event.preventDefault()
return
}
pipelineHandoff.prime()
}}
className={!handoffPayload.trim() ? "pointer-events-none opacity-50" : undefined}
aria-disabled={!handoffPayload.trim()}
>
<Workflow className="mr-2 h-4 w-4" />
{(t.tools["pipeline_builder"] as Record<string, string> | undefined)?.title ?? "Pipeline Builder"}
</Link>
</Button>
</div>
<ToolActionBar actions={actions} handoffPayload={handoffPayload} />
</div>

<SensitiveInputWarning variant="log" />
Expand Down
44 changes: 44 additions & 0 deletions tests/component/http-request-builder-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(),
Expand All @@ -29,10 +44,21 @@ function renderHttpBuilder() {
)
}

function renderHttpBuilderWithChrome() {
return render(
<LangProvider lang="en" translations={getTranslation("en")}>
<RoutePageChrome pathname="/en/http-request-builder">
<HttpRequestBuilderPage />
</RoutePageChrome>
</LangProvider>,
)
}

describe("HttpRequestBuilderPage", () => {
beforeEach(() => {
vi.clearAllMocks()
clipboardWriteMock.mockResolvedValue({ ok: true })
readFavoriteToolKeysMock.mockReturnValue([])
})

it("generates request code without sending a network request", () => {
Expand Down Expand Up @@ -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()
})
})
2 changes: 1 addition & 1 deletion tests/guards/phase1-developer-utilities-pages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
9 changes: 9 additions & 0 deletions tests/guards/tool-action-consistency-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
Expand Down