From c638307421d320edb0ccad73c13024f87bbf5a1b Mon Sep 17 00:00:00 2001 From: Mikey-222 Date: Sun, 18 Oct 2026 19:59:17 +0100 Subject: [PATCH] Add command builder enhancements --- README.md | 4 ++ src/App.test.tsx | 44 ++++++++++++ src/App.tsx | 184 ++++++++++++++++++++++++++++++++++++++++++++--- src/styles.css | 52 ++++++++++++++ src/testkit.ts | 83 +++++++++++++-------- 5 files changed, 331 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index dcdea19..bdd69a2 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ The interface does not execute contracts in the browser. Generated commands run - **Module Filter & Empty State**: Search and filter testing modules by name or description with real-time feedback and an accessible empty-state layout with quick filter reset. - **Responsive & Reduced-Width Navigation**: Accessible navigation layout supporting truncated and responsive labels without overflowing viewport on mobile or narrow screens. - **Required vs. Optional Setting Distinction**: Visual badges (`Required` / `Optional`) and `aria-required` attributes in the CLI workbench distinguishing mandatory parameters from optional configuration flags. +- **Optional Argument Reordering**: Coverage and audit options can be reordered with keyboard-accessible up/down controls so advanced users can put the most important flags first. +- **Safe Reset Confirmation**: Resetting the command builder asks for explicit confirmation before restoring defaults, preventing accidental form resets. +- **Contextual Input Guidance**: Function and ramp fields include inline help text explaining what each value should look like in real contract workflows. +- **Multiline Advanced Editor**: A dedicated multiline editor lets power users review or paste full command snippets and apply them back into the workbench. ## Development diff --git a/src/App.test.tsx b/src/App.test.tsx index 76d1bbd..0983bb2 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -178,3 +178,47 @@ describe("Visual distinction between required and optional settings (#196)", () expect(screen.getByText("Optional")).toBeInTheDocument(); }); }); + +describe("Command builder enhancements", () => { + it("allows optional coverage arguments to be reordered", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Move Open report up" })); + + expect(screen.getByText("soroban-testkit coverage . --open --format html")).toBeInTheDocument(); + }); + + it("requires confirmation before resetting the customized builder", () => { + const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false); + render(); + + fireEvent.click(screen.getByRole("button", { name: "Reset builder" })); + expect(confirmSpy).toHaveBeenCalledWith("Reset the command builder to its default values?"); + expect(screen.getByText("soroban-testkit coverage . --format html --open")).toBeInTheDocument(); + + confirmSpy.mockReturnValue(true); + fireEvent.click(screen.getByRole("button", { name: "Reset builder" })); + expect(screen.getByText("soroban-testkit coverage . --format html --open")).toBeInTheDocument(); + }); + + it("displays contextual help beside function and ramp inputs", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "limits" })); + + expect(screen.getByText(/Use the exported symbol name from your contract ABI/i)).toBeInTheDocument(); + expect(screen.getByText(/Use the parameter you want to increase in the contract input set/i)).toBeInTheDocument(); + }); + + it("supports a multiline advanced command editor", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "Advanced arguments" })); + + const textarea = screen.getByLabelText("Advanced command arguments"); + fireEvent.change(textarea, { + target: { value: "soroban-testkit audit ./src --strict" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Apply multiline command" })); + + expect(screen.getByText("soroban-testkit audit ./src --strict")).toBeInTheDocument(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 0e00718..efc825e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,11 @@ -import { useMemo, useState } from "react"; -import { buildCommand, modules, type CommandName } from "./testkit"; +import { useEffect, useMemo, useState } from "react"; +import { + buildCommand, + formatCommandLine, + modules, + type CommandName, + type OptionalArgKey, +} from "./testkit"; import { copyToClipboard } from "./clipboard"; const descriptions: Record = { @@ -8,6 +14,8 @@ const descriptions: Record = { audit: "Scan contract sources for common auth, arithmetic, and TTL risks.", }; +const defaultCoverageOrder: OptionalArgKey[] = ["format", "open"]; + export default function App() { const [command, setCommand] = useState("coverage"); const [path, setPath] = useState("."); @@ -18,12 +26,91 @@ export default function App() { const [strict, setStrict] = useState(true); const [copied, setCopied] = useState(false); const [moduleFilter, setModuleFilter] = useState(""); + const [coverageOrder, setCoverageOrder] = useState(defaultCoverageOrder); + const [multilineEditor, setMultilineEditor] = useState(false); + const [advancedCommand, setAdvancedCommand] = useState(""); - const generated = useMemo( - () => buildCommand({ command, path, format, open, fn, ramp, strict }), - [command, path, format, open, fn, ramp, strict], + const options = useMemo( + () => ({ + command, + path, + format, + open, + fn, + ramp, + strict, + optionalArgOrder: command === "coverage" ? coverageOrder : command === "audit" ? ["strict"] : undefined, + }), + [command, path, format, open, fn, ramp, strict, coverageOrder], ); + const generated = useMemo(() => buildCommand(options), [options]); + + useEffect(() => { + setAdvancedCommand(formatCommandLine(options, true)); + }, [options]); + + const moveOptionalArg = (key: OptionalArgKey, direction: "up" | "down") => { + setCoverageOrder((current) => { + const index = current.indexOf(key); + if (index < 0) return current; + const nextIndex = direction === "up" ? index - 1 : index + 1; + if (nextIndex < 0 || nextIndex >= current.length) return current; + const next = [...current]; + [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; + return next; + }); + }; + + const resetBuilder = () => { + if (!window.confirm("Reset the command builder to its default values?")) return; + setCommand("coverage"); + setPath("."); + setFormat("html"); + setOpen(true); + setFn("batch_payout"); + setRamp("recipients"); + setStrict(true); + setCoverageOrder(defaultCoverageOrder); + setMultilineEditor(false); + }; + + const applyAdvancedCommand = () => { + const tokens = advancedCommand.match(/"[^"]+"|'[^']+'|[^\s]+/g) ?? []; + if (tokens.length === 0) return; + + const [tool, selectedCommand, ...args] = tokens; + if (tool !== "soroban-testkit") return; + + const nextCommand = selectedCommand as CommandName; + if (nextCommand === "coverage") setCommand("coverage"); + if (nextCommand === "limits") setCommand("limits"); + if (nextCommand === "audit") setCommand("audit"); + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--format" && args[index + 1]) { + setFormat(args[index + 1] as "html" | "lcov" | "text"); + index += 1; + } else if (token === "--open") { + setOpen(true); + } else if (token === "--contract") { + setPath(args[index + 1] ?? "."); + index += 1; + } else if (token === "--fn") { + setFn(args[index + 1] ?? "batch_payout"); + index += 1; + } else if (token === "--ramp") { + setRamp(args[index + 1] ?? "recipients"); + index += 1; + } else if (token === "--strict") { + setStrict(true); + } else if (token && !token.startsWith("--") && !path && !args[index - 1]?.startsWith("--")) { + setPath(token.replace(/^"|"$/g, "")); + } + } + }; + const filteredModules = useMemo(() => { const q = moduleFilter.trim().toLowerCase(); if (!q) return modules; @@ -142,6 +229,9 @@ export default function App() { ))}

{descriptions[command]}

+
+ +
{command === "coverage" && <> +
+
+ Optional arguments + Reorder +
+
+ {[ + { key: "format" as const, label: "Format", summary: "--format html" }, + { key: "open" as const, label: "Open report", summary: "--open" }, + ].map((arg, index, all) => { + const selected = coverageOrder.includes(arg.key); + const currentIndex = coverageOrder.indexOf(arg.key); + return ( +
+ {arg.label} + {arg.summary} +
+ + +
+
+ ); + })} +
+
} {command === "audit" && } -
{generated}
+
+ {generated} + +
+
+ + {multilineEditor && ( + <> +