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
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "latest",
"@testing-library/react": "latest",
"@testing-library/user-event": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"eslint": "latest",
Expand Down
3 changes: 2 additions & 1 deletion src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";

Check failure on line 1 in src/App.test.tsx

View workflow job for this annotation

GitHub Actions / validate

'within' is defined but never used
import userEvent from "@testing-library/user-event";

Check failure on line 2 in src/App.test.tsx

View workflow job for this annotation

GitHub Actions / validate

'userEvent' is defined but never used
import { afterEach, describe, expect, it, vi } from "vitest";
import App from "./App";

Expand Down
291 changes: 289 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,35 @@
const [ramp, setRamp] = useState("recipients");
const [strict, setStrict] = useState(true);
const [copied, setCopied] = useState(false);
const [copiedStep, setCopiedStep] = useState<number | null>(null);

Check failure on line 28 in src/App.tsx

View workflow job for this annotation

GitHub Actions / validate

'setCopiedStep' is assigned a value but never used
const [moduleFilter, setModuleFilter] = useState("");
const [activeWorkflow, setActiveWorkflow] = useState<WorkflowTabId>("coverage");

const generated = useMemo(
() => buildCommand({ command, path, format, open, fn, ramp, strict }),
const options = useMemo<CommandOptions>(
() => ({ command, path, format, open, fn, ramp, strict }),
[command, path, format, open, fn, ramp, strict],
);

const generated = useMemo(() => buildCommand(options), [options]);

const summaryRows = useMemo(() => summarizeConfiguration(options), [options]);
const defaultRows = useMemo(() => effectiveDefaults(options), [options]);
const appliedDefaults = defaultRows.filter((row) => row.applied).length;
const configBCommand = useMemo(
() => (configB ? buildCommand(configB) : ""),
[configB],

Check warning on line 44 in src/App.tsx

View workflow job for this annotation

GitHub Actions / validate

React Hook useMemo has an unnecessary dependency: 'configB'. Either exclude it or remove the dependency array. Outer scope values like 'configB' aren't valid dependencies because mutating them doesn't re-render the component
);
const comparisonRows = useMemo(
() => (configB ? compareConfigurations(options, configB) : []),
[options, configB],

Check warning on line 48 in src/App.tsx

View workflow job for this annotation

GitHub Actions / validate

React Hook useMemo has an unnecessary dependency: 'configB'. Either exclude it or remove the dependency array. Outer scope values like 'configB' aren't valid dependencies because mutating them doesn't re-render the component
);

useEffect(() => {
if (compareOpen) {
comparePanelRef.current?.focus();
}
}, [compareOpen]);

Check warning on line 55 in src/App.tsx

View workflow job for this annotation

GitHub Actions / validate

React Hook useEffect has an unnecessary dependency: 'compareOpen'. Either exclude it or remove the dependency array. Outer scope values like 'compareOpen' aren't valid dependencies because mutating them doesn't re-render the component

const filteredModules = useMemo(() => {
const q = moduleFilter.trim().toLowerCase();
if (!q) return modules;
Expand Down Expand Up @@ -81,6 +102,7 @@
<span>Soroban Testkit</span>
</a>
<div className="nav-links" aria-label="Main navigation">
<a href="#install">Install</a>
<a href="#modules">Modules</a>
<a href="#commands">Commands</a>
<a href="https://github.com/soroban-testkit/testkit-blockchain">GitHub ↗</a>
Expand All @@ -107,6 +129,38 @@
</div>
</section>

<section className="install" id="install" aria-labelledby="install-heading">
<header>
<p className="eyebrow">First-time setup</p>
<h2 id="install-heading">From a clean machine to your first command.</h2>
<p className="install-intro">
Six ordered steps install the Testkit CLI and its prerequisites. Every
step runs locally and keeps contract tests deterministic and offline.
</p>
</header>
<ol className="install-steps">
{installationSteps.map((step, index) => (
<li key={step.title} className="install-step">
<span className="install-step-number" aria-hidden="true">
{String(index + 1).padStart(2, "0")}
</span>
<h3>{step.title}</h3>
<p>{step.description}</p>
<div className="install-command">
<code>{step.command}</code>
<button
type="button"
aria-label={`Copy step ${index + 1} command`}
onClick={() => copyStep(index, step.command)}
>
{copiedStep === index ? "Copied" : "Copy"}
</button>
</div>
</li>
))}
</ol>
</section>

<section className="modules" id="modules">
<header>
<p className="eyebrow">One prelude. Seven focused modules.</p>
Expand Down Expand Up @@ -255,6 +309,33 @@
))}
</div>
<p className="description">{descriptions[command]}</p>

<section className="summary-card" aria-labelledby="command-summary-heading">
<h3 id="command-summary-heading">Command summary</h3>
<p className="summary-workflow">{command}</p>
<dl className="summary-settings">
{summaryRows.map((row) => (
<div className="summary-row" key={row.label}>
<dt>{row.label}</dt>
<dd>{row.value}</dd>
</div>
))}
</dl>
<p className="summary-note">
Updates live from the builder. Values marked (default) apply when a
field is left empty.
</p>
</section>

<button
type="button"
className="comparison-toggle"
aria-expanded={compareOpen}
aria-controls="comparison-panel"
onClick={toggleComparison}
>
Compare two configurations
</button>
</div>
<div className="builder">
<label>
Expand Down Expand Up @@ -311,8 +392,214 @@
</span>
<span className="badge badge-optional">Optional</span>
</label>}

<section className="defaults-preview" aria-labelledby="defaults-preview-heading">
<div className="defaults-preview-header">
<h3 id="defaults-preview-heading">Effective defaults</h3>
{defaultRows.length > 0 && (
<span className="defaults-count">
{appliedDefaults} of {defaultRows.length} applied
</span>
)}
</div>
{defaultRows.length === 0 ? (
<p className="defaults-empty">
This workflow has no empty-field fallbacks. The copied command uses
the values you set above, exactly.
</p>
) : (
<dl className="defaults-list">
{defaultRows.map((row) => (
<div className="defaults-row" key={row.key}>
<dt>{row.label}</dt>
<dd>
<code>{row.applied ? `(empty) → ${row.value}` : row.value}</code>
<span className={`badge ${row.applied ? "badge-required" : "badge-optional"}`}>
{row.applied ? "Default applied" : "From your input"}
</span>
</dd>
</div>
))}
</dl>
)}
</section>

<div className="output"><code>{generated}</code><button onClick={copy}>{copied ? "Copied" : "Copy"}</button></div>
</div>

{compareOpen && configB && (
<section
className="comparison"
id="comparison-panel"
ref={comparePanelRef}
tabIndex={-1}
aria-labelledby="comparison-heading"
>
<div className="comparison-header">
<h3 id="comparison-heading">Compare two configurations</h3>
<p>
Configuration A is the workbench above. Edit configuration B and the
table marks every setting that differs; both commands update as you
type.
</p>
</div>

<fieldset className="comparison-config">
<legend>Configuration B</legend>
<div className="row">
<label>
<div className="field-header"><span>Workflow</span></div>
<select
value={configB.command}
onChange={(event) =>
updateConfigB({ command: event.target.value as CommandName })
}
>
<option value="coverage">coverage</option>
<option value="limits">limits</option>
<option value="audit">audit</option>
</select>
</label>
<label>
<div className="field-header">
<span>Project or contract path</span>
<span className="badge badge-required">Required</span>
</div>
<input
value={configB.path}
onChange={(event) => updateConfigB({ path: event.target.value })}
aria-required="true"
/>
</label>
</div>
{configB.command === "coverage" && (
<div className="row">
<label>
<div className="field-header">
<span>Report format</span>
<span className="badge badge-optional">Optional</span>
</div>
<select
value={configB.format}
onChange={(event) =>
updateConfigB({ format: event.target.value as CommandOptions["format"] })
}
>
<option>html</option>
<option>lcov</option>
<option>text</option>
</select>
</label>
<label className="check">
<span className="check-content">
<input
type="checkbox"
checked={configB.open}
onChange={(event) => updateConfigB({ open: event.target.checked })}
/>
<span>Open report when complete</span>
</span>
<span className="badge badge-optional">Optional</span>
</label>
</div>
)}
{configB.command === "limits" && (
<div className="row">
<label>
<div className="field-header">
<span>Function</span>
<span className="badge badge-required">Required</span>
</div>
<input
value={configB.fn}
onChange={(event) => updateConfigB({ fn: event.target.value })}
aria-required="true"
/>
</label>
<label>
<div className="field-header">
<span>Ramp parameter</span>
<span className="badge badge-required">Required</span>
</div>
<input
value={configB.ramp}
onChange={(event) => updateConfigB({ ramp: event.target.value })}
aria-required="true"
/>
</label>
</div>
)}
{configB.command === "audit" && (
<label className="check">
<span className="check-content">
<input
type="checkbox"
checked={configB.strict}
onChange={(event) => updateConfigB({ strict: event.target.checked })}
/>
<span>Fail on warnings</span>
</span>
<span className="badge badge-optional">Optional</span>
</label>
)}
<button
type="button"
className="button secondary comparison-sync"
onClick={() => setConfigB({ ...options })}
>
Match B to A
</button>
</fieldset>

<div
className="comparison-table-wrap"
role="region"
aria-label="Configuration differences"
tabIndex={0}
>
<table
className="comparison-table"
aria-label="Setting comparison between configuration A and configuration B"
>
<thead>
<tr>
<th scope="col">Setting</th>
<th scope="col">Configuration A</th>
<th scope="col">Configuration B</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{comparisonRows.map((row) => (
<tr key={row.key} data-changed={row.changed}>
<th scope="row">{row.label}</th>
<td>{row.a}</td>
<td>{row.b}</td>
<td>
{row.changed ? (
<span className="badge badge-required">Changed</span>
) : (
<span className="comparison-status">Matches</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>

<div className="comparison-commands">
<div>
<h4>Configuration A command</h4>
<pre className="comparison-command"><code>{generated}</code></pre>
</div>
<div>
<h4>Configuration B command</h4>
<pre className="comparison-command"><code>{configBCommand}</code></pre>
</div>
</div>
</section>
)}
</section>

<footer><span>Built for the Stellar developer ecosystem.</span><span>Apache-2.0</span></footer>
Expand Down
Loading
Loading