diff --git a/README.md b/README.md index b81a78c..cbac473 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The interface does not execute contracts in the browser. Generated commands run - **Landscape Phone Optimization**: Condensed command form layout for landscape phone orientations (541px-900px width, max 600px height). - **Required vs. Optional Setting Distinction**: Visual badges (`Required` / `Optional`) and `aria-required` attributes in the CLI workbench distinguishing mandatory parameters from optional configuration flags. - **Audit workflow support**: An expandable, keyboard-accessible preflight checklist, strict-mode control, and advanced multiline command editor help users prepare and reproduce source audits. -- **Limits command reference**: A standalone reference explains the ready-to-run limits command, its `--contract`, `--fn`, and `--ramp` arguments, defaults, and offline/read-only behavior. +- **Limits command reference**: A standalone, keyboard-accessible walkthrough builds the Sororail batch-payout contract and runs `soroban-testkit limits` against its `execute_equal` entry point, alongside argument defaults and offline/read-only behavior. ## Development diff --git a/docs/accessibility.md b/docs/accessibility.md index 333dd19..9047724 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -11,6 +11,7 @@ automatically, what was fixed, and what still needs a manual pass. | Keyboard | Skip link is first in tab order and moves focus to the page content | `tests/visual/accessibility.spec.ts` | | Keyboard | Every focusable control shows an outline on keyboard focus | `tests/visual/accessibility.spec.ts` | | Keyboard | Workflow tabs respond to arrow, Home and End keys | `src/App.test.tsx`, `tests/visual/accessibility.spec.ts` | +| Keyboard | Each command in the limits example is reachable in order with Tab | `tests/visual/accessibility.spec.ts` | | Screen reader | Landmarks, a single `h1`, and named navigation links | `tests/visual/accessibility.spec.ts` | | Screen reader | Command selector is a labelled group of toggle buttons | `src/App.test.tsx`, `tests/visual/accessibility.spec.ts` | | Screen reader | Copy results are announced through a polite live region | `src/App.test.tsx`, `tests/visual/accessibility.spec.ts` | @@ -48,6 +49,6 @@ browser and screen reader pair per platform. | 5 | Module numbers (`01`–`07`) were read as text before every module name. | Fixed: `aria-hidden`. | `src/App.tsx` | | 6 | The `footer` sits inside `main`, so it is not exposed as a `contentinfo` landmark. | Open. Moving it out of `main` changes the wide-screen layout, so it needs a visual check. | `src/App.tsx` | | 7 | Collapsed guide toggles set `aria-controls` to panels that are not rendered until opened. | Open. Minor; the attribute points at a missing id while collapsed. | `src/App.tsx` | -| 8 | The limits workflow needed a discoverable command reference. | Fixed: added a focusable reference section with semantic headings, lists, code examples, and a keyboard-accessible navigation link. | `src/App.tsx`, `src/styles.css` | +| 8 | The limits workflow needed a discoverable, ready-to-run command reference. | Fixed: added a focusable reference section and a build-and-run example with keyboard-focusable command steps. | `src/App.tsx`, `src/styles.css`, `tests/visual/accessibility.spec.ts` | | 9 | Scrollable `pre` blocks are not keyboard-focusable in every browser, so long lines may be unreachable by keyboard on some browsers. | Open. | `src/styles.css` | | 10 | The stored visual baselines in `tests/visual/__screenshots__` predate the current layout and do not match a fresh render. | Open. Baselines should be regenerated on the CI platform, not locally. | `tests/visual/` | diff --git a/src/App.test.tsx b/src/App.test.tsx index 9c9efd2..68187d5 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -132,7 +132,7 @@ describe("Module filter and empty-state layout (#188)", () => { expect(screen.getByRole("heading", { name: /No modules match .zzz./ })).toBeInTheDocument(); expect(document.getElementById("module-grid")).toBeNull(); - const resetButton = screen.getByRole("button", { name: "Reset filter" }); + const resetButton = screen.getByRole("button", { name: "Reset filters" }); resetButton.focus(); fireEvent.click(resetButton); @@ -401,6 +401,25 @@ describe("Limits command reference (#92)", () => { expect(link).toHaveAttribute("href", "#limits-reference"); expect(document.getElementById("limits-reference")).toHaveAttribute("tabindex", "-1"); }); + + it("documents a complete limits run against a real contract with keyboard-accessible commands", () => { + render(); + + expect(screen.getByRole("heading", { name: "Run a complete example" })).toBeInTheDocument(); + expect(screen.getByText(/Rust 1\.91 or later/i)).toBeInTheDocument(); + expect(screen.getByRole("region", { name: "Clone the Sororail contract workspace" })) + .toHaveTextContent("git clone https://github.com/Sororail/sororail-contracts.git"); + expect(screen.getByRole("region", { name: "Add the wasm32v1-none target" })) + .toHaveTextContent("rustup target add wasm32v1-none"); + expect(screen.getByRole("region", { name: "Build the batch-payout contract" })) + .toHaveTextContent("contracts/batch_payout/Cargo.toml"); + + const runCommand = screen.getByRole("region", { name: "Run the limits example" }); + expect(runCommand).toHaveTextContent("soroban-testkit limits"); + expect(runCommand).toHaveTextContent("--fn execute_equal"); + expect(runCommand).toHaveTextContent("--ramp recipients"); + expect(runCommand).toHaveAttribute("tabindex", "0"); + }); }); describe("Audit workflow prerequisites checklist (#91)", () => { diff --git a/src/App.tsx b/src/App.tsx index de74841..e9fc59b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,7 @@ const navSectionIds = navSections.map((section) => section.id); // Which CLI workflows are most useful for each module (#130). Keyed by module // name so adding a module without a mapping fails type-checking. -const moduleRelatedWorkflows: Record<(typeof modules)[number][0], CommandName[]> = { +const moduleRelatedWorkflows: Record<(typeof modules)[number]["name"], CommandName[]> = { core: ["coverage"], ledger: ["audit"], money: ["audit", "limits"], @@ -239,12 +239,16 @@ export default function App() { // polite live region announces the result explicitly (#108). const [announcement, setAnnouncement] = useState(""); const [moduleFilter, setModuleFilter] = useState(""); + const [categoryFilter, setCategoryFilter] = useState(null); + const [selectedModule, setSelectedModule] = useState(null); + const [showBackToTop, setShowBackToTop] = useState(false); const moduleSearchRef = useRef(null); // Resetting from the empty state removes the button that had focus, so move // focus back to the search box instead of dropping it to (#123). const resetModuleFilter = () => { setModuleFilter(""); + setCategoryFilter(null); moduleSearchRef.current?.focus(); }; const [coverageOrder, setCoverageOrder] = useState(defaultCoverageOrder); @@ -600,71 +604,21 @@ export default function App() {
-
-

One prelude. Seven focused modules.

-

Everything a serious contract test needs.

-
- - setModuleFilter(event.target.value)} - aria-label="Filter modules" - /> - {moduleFilter && ( - - )} -
-
- - {filteredModules.length > 0 ? ( -
- {filteredModules.map(([name, description], index) => ( - - ))} -
- ) : ( -
- -

No modules match “{moduleFilter}”

-

No testing modules found matching your filter criteria. Try adjusting your search term.

- -
+ ) : ( <>
@@ -672,10 +626,11 @@ export default function App() {

Everything a serious contract test needs.

0 ? (
{filteredModules.map((mod, index) => ( -
setSelectedModule(mod.name)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setSelectedModule(mod.name); - } - }} - > - 0{index + 1} -

{mod.name}

-

{mod.description}

- {mod.category} + ))}
@@ -746,20 +710,17 @@ export default function App() { -

No modules match your filters

+

{moduleFilter ? `No modules match “${moduleFilter}”` : "No modules match your filters"}

- {moduleFilter - ? `No testing modules found matching “${moduleFilter}”${categoryFilter ? ` in ${categoryFilter}` : ""}.` - : `No testing modules in the ${categoryFilter} category.`} + {categoryFilter + ? `No testing modules in the ${categoryFilter} category match this search.` + : "No testing modules match this search."} {" "}Try adjusting your filters.

@@ -1438,9 +1399,9 @@ export default function App() {
-

Ready-to-run command

+

Command shape

{"soroban-testkit limits \\\n  --contract target/wasm32v1-none/release/contract.wasm \\\n  --fn batch_payout \\\n  --ramp recipients"}
-

Replace the function and ramp names with exported contract inputs from your ABI.

+

Use the compiled WASM path and exported function and input names from your contract.

Arguments

@@ -1460,6 +1421,48 @@ export default function App() {

Reads: local WASM and export metadata. Writes: none.

+
+

Run a complete example

+

+ This uses the public Sororail batch-payout contract, whose execute_equal entry point + ramps the recipients input. Install Rust 1.91 or later and + {" "}install soroban-testkit before starting. +

+
    +
  1. +

    Get the contract workspace

    +
    +                  {"git clone https://github.com/Sororail/sororail-contracts.git\ncd sororail-contracts"}
    +                
    +
  2. +
  3. +

    Add the WASM target

    +
    +                  rustup target add wasm32v1-none
    +                
    +
  4. +
  5. +

    Build the batch-payout contract

    +
    +                  
    +                    cargo build --release --target wasm32v1-none --manifest-path contracts/batch_payout/Cargo.toml
    +                  
    +                
    +
  6. +
  7. +

    Run the limits workflow from the workspace root

    +
    +                  
    +                    {"soroban-testkit limits \\\n  --contract target/wasm32v1-none/release/sororail_batch_payout.wasm \\\n  --fn execute_equal \\\n  --ramp recipients"}
    +                  
    +                
    +
  8. +
+

+ The result reports the last successful recipient count along with instruction and memory usage; + measured limits can vary with the contract build and runtime. +

+
diff --git a/src/styles.css b/src/styles.css index df24c94..6a908e8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -194,6 +194,20 @@ h2 { margin: var(--space-md) 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacin .module-grid span { color: var(--color-text-module-num); font: .7rem var(--font-mono); } .module-grid h3 { margin: var(--space-6xl) 0 9px; color: var(--color-accent); font: 500 1.1rem var(--font-mono); } .module-grid p { margin: 0; color: var(--color-text-body); line-height: 1.5; font-size: .87rem; } +.module-card-select { display: block; width: 100%; padding: 0; border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; } +.module-card-select:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; } +.module-card-links, .category-filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); } +.module-card-links { margin-top: var(--space-xl); } +.module-card-links a { color: var(--color-accent); font-size: .75rem; } +.category-filter-bar { margin: var(--space-xl) 0; } +.category-chip { padding: var(--space-xs) var(--space-md); border: 1px solid var(--color-border-default); border-radius: var(--radius-round); color: var(--color-text-secondary); background: transparent; cursor: pointer; } +.category-chip.active { border-color: var(--color-accent); color: var(--color-accent); background: var(--color-accent-subtle); } +.module-category-tag { display: inline-block; margin-top: var(--space-md); } +.module-detail { padding: var(--space-5xl); background: var(--color-surface-card); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); } +.module-detail nav { display: flex; height: auto; justify-content: flex-start; gap: var(--space-md); padding: 0; border: 0; margin-bottom: var(--space-3xl); } +.module-detail nav a { color: var(--color-accent); } +.module-detail h2 { margin: var(--space-md) 0; } +.module-detail > p:not(.eyebrow) { color: var(--color-text-body); line-height: 1.65; } /* Module Empty State */ .module-empty-state { @@ -237,6 +251,12 @@ h2 { margin: var(--space-md) 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacin .reference-arguments dt { color: var(--color-text-code); font-family: var(--font-mono); font-size: .8rem; } .reference-arguments dd { margin: var(--space-xs) 0 0; } .reference-note { margin-bottom: 0; color: var(--color-text-muted) !important; font-family: var(--font-mono); font-size: .75rem !important; } +.reference-example { margin-top: var(--space-xl); } +.reference-example-steps { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-xl); margin: 0; padding-left: 1.25rem; } +.reference-example-steps li { min-width: 0; padding-left: var(--space-xs); } +.reference-example-steps h4 { margin: 0 0 var(--space-md); color: var(--color-text-primary); font-size: .85rem; } +.reference-example-steps pre { overflow-wrap: anywhere; } +.reference-example-steps pre:focus { outline: 2px solid var(--color-accent); outline-offset: 3px; } /* Commands / CLI Workbench */ .commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: var(--color-surface); } @@ -308,7 +328,7 @@ footer { display: flex; justify-content: space-between; padding: 34px var(--page @media (max-width: 900px) { .hero, .commands { grid-template-columns: 1fr; } .hero { padding-top: 70px; } - .reference-grid { grid-template-columns: 1fr; } + .reference-grid, .reference-example-steps { grid-template-columns: 1fr; } } /* Tablet-specific spacing for section headers (#186) */ diff --git a/tests/visual/accessibility.spec.ts b/tests/visual/accessibility.spec.ts index f6b4fd9..1f26311 100644 --- a/tests/visual/accessibility.spec.ts +++ b/tests/visual/accessibility.spec.ts @@ -94,6 +94,24 @@ test.describe("screen reader structure (#108)", () => { await expect(page.locator('[aria-live="polite"]').filter({ hasText: "Command copied to clipboard" })).toHaveCount(1); }); + + test("limits example commands are keyboard-focusable in workflow order", async ({ page }) => { + await page.goto("/"); + + const cloneCommand = page.getByRole("region", { name: "Clone the Sororail contract workspace" }); + const targetCommand = page.getByRole("region", { name: "Add the wasm32v1-none target" }); + const buildCommand = page.getByRole("region", { name: "Build the batch-payout contract" }); + const runCommand = page.getByRole("region", { name: "Run the limits example" }); + + await cloneCommand.focus(); + await expect(cloneCommand).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(targetCommand).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(buildCommand).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(runCommand).toBeFocused(); + }); }); // 200% zoom on a 1280px layout is 640 CSS px. WCAG 1.4.10 reflow is 320 CSS px