Configure a supported Testkit workflow and copy the exact command to your terminal.
-
-
- {command === "coverage" && <>
-
-
- >}
- {command === "limits" &&
}
- {command === "audit" &&
}
-
{generated}
+
+
+
+ setPath(event.target.value)}
+ aria-invalid={Boolean(errors.path)}
+ aria-describedby={errors.path ? "error-path" : undefined}
+ />
+ {errors.path && (
+
+ {errors.path}
+
+ )}
+
+
+ {command === "coverage" && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ {command === "limits" && (
+
+ )}
+
+ {command === "audit" && (
+
+ )}
+
+
+ {generated}
+
+
+
+
+ {announcement}
+
diff --git a/src/styles.css b/src/styles.css
index 332b692..92f6e13 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -33,10 +33,19 @@ section header { max-width: 650px; margin-bottom: 50px; } h2 { margin: 12px 0; f
.commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: #091916; }.command-copy > p:not(.eyebrow) { color: #91a39c; line-height: 1.7; }
.command-tabs { display: flex; gap: 8px; margin-top: 36px; }.command-tabs button { border: 1px solid #ffffff20; color: #8ea099; background: transparent; padding: 9px 14px; border-radius: 5px; cursor: pointer; font: .75rem "DM Mono"; }.command-tabs button.active { border-color: #66e2ad; color: #66e2ad; background: #66e2ad0b; }
.description { min-height: 48px; }.builder { padding: 32px; background: #061310; border: 1px solid #ffffff18; border-radius: 10px; }
-label { display: grid; gap: 9px; margin-bottom: 22px; color: #a7b8b1; font-size: .78rem; }.row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
+.field, label { display: grid; gap: 9px; margin-bottom: 22px; color: #a7b8b1; font-size: .78rem; }
+.field label { margin-bottom: 0; }
+.row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
input, select { width: 100%; padding: 12px 13px; color: #eaf6f1; background: #ffffff06; border: 1px solid #ffffff1f; border-radius: 5px; outline: none; } input:focus, select:focus { border-color: #66e2ad; }
.check { display: flex; align-items: center; grid-template-columns: auto 1fr; }.check input { width: auto; accent-color: #66e2ad; }
+.field-error { color: #ff6b6b; font-size: .72rem; line-height: 1.4; }
+input[aria-invalid="true"] { border-color: #ff6b6b; }
.output { display: flex; align-items: center; gap: 15px; padding: 16px; background: #020807; border-radius: 6px; }.output code { flex: 1; color: #66e2ad; font: .75rem/1.6 "DM Mono"; overflow-wrap: anywhere; }.output button { color: #061310; background: #66e2ad; border: 0; border-radius: 4px; padding: 8px 12px; cursor: pointer; }
+.output button.button-reset { color: #a9bbb4; background: transparent; border: 1px solid #ffffff24; }
+.output button.button-reset:hover { color: #e9f4ef; border-color: #66e2ad; }
+.output button.error { background: #ff5f56; color: #ffffff; }
+.output button:disabled { opacity: 0.45; cursor: not-allowed; }
+.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
footer { display: flex; justify-content: space-between; padding: 34px 5vw; border-top: 1px solid #ffffff12; color: #6f817a; font-size: .78rem; }
@media (max-width: 900px) { .hero, .commands { grid-template-columns: 1fr; }.hero { padding-top: 70px; }.module-grid { grid-template-columns: repeat(2, 1fr); }.nav-links a:not(:last-child) { display: none; } }
@media (max-width: 540px) { nav, .hero, .modules, .commands { padding-left: 22px; padding-right: 22px; }.module-grid { grid-template-columns: 1fr; }.row { grid-template-columns: 1fr; }.actions { flex-direction: column; }.terminal pre { padding: 22px; font-size: .7rem; } footer { gap: 20px; } }
diff --git a/src/testkit.test.ts b/src/testkit.test.ts
index f03c0b1..b12dc4f 100644
--- a/src/testkit.test.ts
+++ b/src/testkit.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { buildCommand } from "./testkit";
+import { buildCommand, validateCommandInputs } from "./testkit";
describe("buildCommand", () => {
it("builds a coverage command", () => {
@@ -47,3 +47,120 @@ describe("buildCommand", () => {
.toBe('soroban-testkit audit "./a b c"');
});
});
+
+describe("validateCommandInputs", () => {
+ it("returns no errors for valid coverage options", () => {
+ const errors = validateCommandInputs({
+ command: "coverage",
+ path: ".",
+ format: "html",
+ open: true,
+ fn: "",
+ ramp: "",
+ strict: false,
+ });
+ expect(errors).toEqual({});
+ });
+
+ it("returns an error when path is empty or whitespace only", () => {
+ const emptyErrors = validateCommandInputs({
+ command: "coverage",
+ path: "",
+ format: "html",
+ open: true,
+ fn: "",
+ ramp: "",
+ strict: false,
+ });
+ expect(emptyErrors.path).toBe("Project or contract path cannot be empty.");
+
+ const whitespaceErrors = validateCommandInputs({
+ command: "coverage",
+ path: " \t",
+ format: "html",
+ open: true,
+ fn: "",
+ ramp: "",
+ strict: false,
+ });
+ expect(whitespaceErrors.path).toBe("Project or contract path cannot be empty.");
+ });
+
+ it("returns no errors for valid limits options", () => {
+ const errors = validateCommandInputs({
+ command: "limits",
+ path: "target/wasm32v1-none/release/contract.wasm",
+ format: "html",
+ open: true,
+ fn: "batch_payout",
+ ramp: "recipients",
+ strict: false,
+ });
+ expect(errors).toEqual({});
+ });
+
+ it("validates function name in limits as a valid Rust identifier", () => {
+ const emptyFn = validateCommandInputs({
+ command: "limits",
+ path: "target.wasm",
+ format: "html",
+ open: true,
+ fn: "",
+ ramp: "recipients",
+ strict: false,
+ });
+ expect(emptyFn.fn).toBe("Function name cannot be empty.");
+
+ const invalidFn = validateCommandInputs({
+ command: "limits",
+ path: "target.wasm",
+ format: "html",
+ open: true,
+ fn: "123_invalid-name!",
+ ramp: "recipients",
+ strict: false,
+ });
+ expect(invalidFn.fn).toBe(
+ "Function name must be a valid Rust identifier (e.g. batch_payout).",
+ );
+ });
+
+ it("validates ramp parameter in limits as a valid Rust identifier", () => {
+ const emptyRamp = validateCommandInputs({
+ command: "limits",
+ path: "target.wasm",
+ format: "html",
+ open: true,
+ fn: "batch_payout",
+ ramp: "",
+ strict: false,
+ });
+ expect(emptyRamp.ramp).toBe("Ramp parameter cannot be empty.");
+
+ const invalidRamp = validateCommandInputs({
+ command: "limits",
+ path: "target.wasm",
+ format: "html",
+ open: true,
+ fn: "batch_payout",
+ ramp: "invalid ramp param",
+ strict: false,
+ });
+ expect(invalidRamp.ramp).toBe(
+ "Ramp parameter must be a valid Rust identifier (e.g. recipients).",
+ );
+ });
+
+ it("returns no errors for valid audit options", () => {
+ const errors = validateCommandInputs({
+ command: "audit",
+ path: "./src",
+ format: "text",
+ open: false,
+ fn: "",
+ ramp: "",
+ strict: true,
+ });
+ expect(errors).toEqual({});
+ });
+});
diff --git a/src/testkit.ts b/src/testkit.ts
index 32bad1e..22a1718 100644
--- a/src/testkit.ts
+++ b/src/testkit.ts
@@ -54,3 +54,58 @@ export function buildCommand(options: CommandOptions): string {
.filter(Boolean)
.join(" ");
}
+
+export const DEFAULT_OPTIONS: Record
= {
+ coverage: {
+ command: "coverage",
+ path: ".",
+ format: "html",
+ open: true,
+ fn: "batch_payout",
+ ramp: "recipients",
+ strict: true,
+ },
+ limits: {
+ command: "limits",
+ path: "target/wasm32v1-none/release/contract.wasm",
+ format: "html",
+ open: true,
+ fn: "batch_payout",
+ ramp: "recipients",
+ strict: true,
+ },
+ audit: {
+ command: "audit",
+ path: "./src",
+ format: "text",
+ open: false,
+ fn: "batch_payout",
+ ramp: "recipients",
+ strict: true,
+ },
+};
+
+export function validateCommandInputs(options: CommandOptions): Record {
+ const errors: Record = {};
+
+ if (!options.path || options.path.trim() === "") {
+ errors.path = "Project or contract path cannot be empty.";
+ }
+
+ if (options.command === "limits") {
+ if (!options.fn || options.fn.trim() === "") {
+ errors.fn = "Function name cannot be empty.";
+ } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(options.fn.trim())) {
+ errors.fn = "Function name must be a valid Rust identifier (e.g. batch_payout).";
+ }
+
+ if (!options.ramp || options.ramp.trim() === "") {
+ errors.ramp = "Ramp parameter cannot be empty.";
+ } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(options.ramp.trim())) {
+ errors.ramp = "Ramp parameter must be a valid Rust identifier (e.g. recipients).";
+ }
+ }
+
+ return errors;
+}
+
diff --git a/tests/visual/landing.spec.ts b/tests/visual/landing.spec.ts
index e186581..556c166 100644
--- a/tests/visual/landing.spec.ts
+++ b/tests/visual/landing.spec.ts
@@ -19,10 +19,10 @@ test.describe("landing page visual regression", () => {
await expect(commands).toHaveScreenshot("commands-coverage.png");
- await page.getByRole("button", { name: "limits" }).click();
+ await page.getByRole("tab", { name: "limits" }).click();
await expect(commands).toHaveScreenshot("commands-limits.png");
- await page.getByRole("button", { name: "audit" }).click();
+ await page.getByRole("tab", { name: "audit" }).click();
await expect(commands).toHaveScreenshot("commands-audit.png");
});