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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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/` |
21 changes: 20 additions & 1 deletion src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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(<App />);

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)", () => {
Expand Down
191 changes: 97 additions & 94 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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<ModuleCategory | null>(null);
const [selectedModule, setSelectedModule] = useState<string | null>(null);
const [showBackToTop, setShowBackToTop] = useState(false);
const moduleSearchRef = useRef<HTMLInputElement>(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 <body> (#123).
const resetModuleFilter = () => {
setModuleFilter("");
setCategoryFilter(null);
moduleSearchRef.current?.focus();
};
const [coverageOrder, setCoverageOrder] = useState<OptionalArgKey[]>(defaultCoverageOrder);
Expand Down Expand Up @@ -600,82 +604,33 @@ export default function App() {
</section>

<section className="modules" id="modules" tabIndex={-1}>
<header>
<p className="eyebrow">One prelude. Seven focused modules.</p>
<h2>Everything a serious contract test needs.</h2>
<div className="module-filter-bar">
<label htmlFor="module-search" className="sr-only">
Filter modules
</label>
<input
id="module-search"
ref={moduleSearchRef}
type="search"
className="module-filter-input"
placeholder="Filter modules by name or description..."
value={moduleFilter}
onChange={(event) => setModuleFilter(event.target.value)}
aria-label="Filter modules"
/>
{moduleFilter && (
<button
type="button"
className="button secondary"
onClick={() => setSelectedModule(null)}
>
← Back to all modules
</button>
)}
</div>
</header>

{filteredModules.length > 0 ? (
<div className="module-grid" id="module-grid">
{filteredModules.map(([name, description], index) => (
<article key={name}>
<span aria-hidden="true">0{index + 1}</span>
<h3>{name}</h3>
<p>{description}</p>
<div className="module-card-links">
<a href="#commands" aria-label={`Open the command builder from the ${name} module`}>
Command builder
</a>
{moduleRelatedWorkflows[name].map((workflow) => (
<a
key={workflow}
href="#commands"
aria-label={`Open the ${workflow} workflow related to ${name}`}
onClick={() => setCommand(workflow)}
>
{workflow}
</a>
))}
</div>
</article>
))}
</div>
) : (
<div className="module-empty-state" role="status" aria-live="polite">
<div className="module-empty-icon" aria-hidden="true">
∅
</div>
<h3>No modules match &ldquo;{moduleFilter}&rdquo;</h3>
<p>No testing modules found matching your filter criteria. Try adjusting your search term.</p>
<button type="button" className="button secondary" onClick={resetModuleFilter}>
Reset filter
{selectedModuleData ? (
<article className="module-detail">
<nav aria-label="Breadcrumb navigation">
<a href="#top">Home</a>
<span aria-hidden="true">/</span>
<a href="#modules" onClick={() => setSelectedModule(null)}>Modules</a>
<span aria-current="page">{selectedModuleData.name}</span>
</nav>
<p className="eyebrow">{selectedModuleData.category}</p>
<h2>{selectedModuleData.name}</h2>
<p>{selectedModuleData.description}</p>
<button type="button" className="button secondary" onClick={() => setSelectedModule(null)}>
Back to all modules
</button>
</div>
</article>
) : (
<>
<header>
<p className="eyebrow">One prelude. Seven focused modules.</p>
<h2>Everything a serious contract test needs.</h2>
<div className="module-filter-bar">
<label htmlFor="module-search" className="sr-only">
Search modules by name or description
Filter modules
</label>
<input
id="module-search"
ref={moduleSearchRef}
type="search"
className="module-filter-input"
placeholder="Search modules by name or description… (press /)"
Expand Down Expand Up @@ -720,24 +675,33 @@ export default function App() {
{filteredModules.length > 0 ? (
<div className="module-grid" id="module-grid">
{filteredModules.map((mod, index) => (
<article
key={mod.name}
className="module-card"
tabIndex={0}
role="button"
aria-label={`View details for ${mod.name} module`}
onClick={() => setSelectedModule(mod.name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setSelectedModule(mod.name);
}
}}
>
<span>0{index + 1}</span>
<h3>{mod.name}</h3>
<p>{mod.description}</p>
<span className="module-category-tag">{mod.category}</span>
<article key={mod.name} className="module-card">
<button
type="button"
className="module-card-select"
aria-label={`View details for ${mod.name} module`}
onClick={() => setSelectedModule(mod.name)}
>
<span aria-hidden="true">0{index + 1}</span>
<h3>{mod.name}</h3>
<p>{mod.description}</p>
<span className="module-category-tag">{mod.category}</span>
</button>
<div className="module-card-links">
<a href="#commands" aria-label={`Open the command builder from the ${mod.name} module`}>
Command builder
</a>
{moduleRelatedWorkflows[mod.name].map((workflow) => (
<a
key={workflow}
href="#commands"
aria-label={`Open the ${workflow} workflow related to ${mod.name}`}
onClick={() => setCommand(workflow)}
>
{workflow}
</a>
))}
</div>
</article>
))}
</div>
Expand All @@ -746,20 +710,17 @@ export default function App() {
<div className="module-empty-icon" aria-hidden="true">
∅
</div>
<h3>No modules match your filters</h3>
<h3>{moduleFilter ? `No modules match “${moduleFilter}”` : "No modules match your filters"}</h3>
<p>
{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.
</p>
<button
type="button"
className="button secondary"
onClick={() => {
setModuleFilter("");
setCategoryFilter(null);
}}
onClick={resetModuleFilter}
>
Reset filters
</button>
Expand Down Expand Up @@ -1438,9 +1399,9 @@ export default function App() {
</div>
<div className="reference-grid">
<article className="reference-card">
<h3>Ready-to-run command</h3>
<h3>Command shape</h3>
<pre aria-label="Resource limits command example"><code>{"soroban-testkit limits \\\n --contract target/wasm32v1-none/release/contract.wasm \\\n --fn batch_payout \\\n --ramp recipients"}</code></pre>
<p>Replace the function and ramp names with exported contract inputs from your ABI.</p>
<p>Use the compiled WASM path and exported function and input names from your contract.</p>
</article>
<article className="reference-card">
<h3>Arguments</h3>
Expand All @@ -1460,6 +1421,48 @@ export default function App() {
<p className="reference-note">Reads: local WASM and export metadata. Writes: none.</p>
</article>
</div>
<article className="reference-card reference-example" aria-labelledby="limits-example-heading">
<h3 id="limits-example-heading">Run a complete example</h3>
<p>
This uses the public Sororail batch-payout contract, whose <code>execute_equal</code> entry point
ramps the <code>recipients</code> input. Install Rust 1.91 or later and
{" "}<a href={INSTALL_GUIDE_URL}>install soroban-testkit</a> before starting.
</p>
<ol className="reference-example-steps">
<li>
<h4>Get the contract workspace</h4>
<pre role="region" aria-label="Clone the Sororail contract workspace" tabIndex={0}>
<code>{"git clone https://github.com/Sororail/sororail-contracts.git\ncd sororail-contracts"}</code>
</pre>
</li>
<li>
<h4>Add the WASM target</h4>
<pre role="region" aria-label="Add the wasm32v1-none target" tabIndex={0}>
<code>rustup target add wasm32v1-none</code>
</pre>
</li>
<li>
<h4>Build the batch-payout contract</h4>
<pre role="region" aria-label="Build the batch-payout contract" tabIndex={0}>
<code>
cargo build --release --target wasm32v1-none --manifest-path contracts/batch_payout/Cargo.toml
</code>
</pre>
</li>
<li>
<h4>Run the limits workflow from the workspace root</h4>
<pre role="region" aria-label="Run the limits example" tabIndex={0}>
<code>
{"soroban-testkit limits \\\n --contract target/wasm32v1-none/release/sororail_batch_payout.wasm \\\n --fn execute_equal \\\n --ramp recipients"}
</code>
</pre>
</li>
</ol>
<p className="reference-note">
The result reports the last successful recipient count along with instruction and memory usage;
measured limits can vary with the contract build and runtime.
</p>
</article>
</section>
</section>

Expand Down
Loading
Loading