Skip to content

test(frontend): cover stream-creation amount, schedule, token and tx-lifecycle paths - #1802

Open
Purity-Euphemia wants to merge 1 commit into
LabsCrypt:mainfrom
Purity-Euphemia:test/issues-1794-1795-1797-1798
Open

Purity-Euphemia wants to merge 1 commit into
LabsCrypt:mainfrom
Purity-Euphemia:test/issues-1794-1795-1797-1798

Conversation

@Purity-Euphemia

Copy link
Copy Markdown

Summary

This PR closes the four open testing gaps raised by the Stellar Wave sweep on the
stream-creation flow. It adds 83 deterministic tests across five new test files that
cover the success, failure, boundary and dependency-failure paths of the four modules
flagged in the issues, and it fixes one real bug that the new tests uncovered (the
flat create-stream form never showed the first validation error as a toast).

Closes #1794
Closes #1795
Closes #1797
Closes #1798


What was missing

Issue Module Gap
#1794 frontend/src/app/streams/create/create-stream-content.tsx Polling success, timeout, cancellation and failed-transaction states were untested
#1795 frontend/src/components/stream-creation/AmountStep.tsx Token decimals, zero amount and excessive precision validation were untested
#1797 frontend/src/components/stream-creation/ScheduleStep.tsx Timezone conversion, past dates and daylight-saving boundaries were untested
#1798 frontend/src/components/stream-creation/TokenStep.tsx Missing token metadata, unsupported decimals and failed token lookup were untested

Changes

1. #1794 — Create-stream transaction lifecycle

frontend/src/lib/soroban.polling.test.ts (new, 7 tests)

Exercises the real confirmation-polling loop in lib/soroban.ts (freighterCall) with a
deterministic fake RPC and fake wallet:

  • success — the transaction is confirmed on the 3rd poll and { success, txHash } is
    returned (asserts the poll count and the polled tx hash);
  • failed transaction — a FAILED status raises SorobanCallError("Transaction failed on-chain.", "NetworkError");
  • timeout — a transaction that never reaches a terminal state is polled 20 times, one
    second apart, and then raises SorobanCallError("Transaction confirmation timed out.");
  • cancellation — a wallet rejection raises
    SorobanCallError("Transaction was rejected in wallet.", "WalletRejected") and no poll
    is performed;
  • broadcast failure, simulation failure and multisig routing are covered as
    additional dependency-failure paths.

Fake timers (vi.advanceTimersByTimeAsync) keep the polling tests deterministic and fast
(26 ms instead of 24 s of wall-clock waiting).

frontend/src/app/streams/create/__tests__/create-stream-content.tx-lifecycle.test.tsx
(new, 14 tests)

Covers the page-level lifecycle the user actually sees:

  • the stream is submitted with the correctly converted amount (100 → 1_000_000_000n
    stroops) and duration (30 days → 2_592_000n);
  • the button reports Confirm in Wallet... while the transaction is pending and
    Finalizing Stream... once it is confirmed;
  • on success the draft is discarded, the success toast is shown and the router is
    redirected to /dashboard after the 2 s grace period, with the form enabled again;
  • timeout, failed transaction, insufficient balance and cancellation each
    surface the exact user-facing message produced by toSorobanErrorMessage, never
    redirect, log the failure and re-enable the form;
  • validation failures (malformed recipient, amount above the wallet balance) and
    dependency failures (balance lookup error) are asserted: nothing is submitted to the
    network and the user gets actionable feedback.

2. #1795 — Amount step validation

frontend/src/components/stream-creation/__tests__/AmountStep.test.tsx (new, 22 tests)

  • token decimals — exactly 7 decimal places are accepted, 8 are rejected with
    Amount exceeds maximum precision (7 decimal places); the boundary (6 vs. 8 decimals)
    and negative amounts are pinned down;
  • zero amount — "0", "0.0" and "" are rejected (Amount must be a positive number / Amount is required) and the step does not render the “You will stream …”
    summary for a zero amount;
  • excessive precision — the error is rendered with role="alert", the input is marked
    aria-invalid and wired to aria-describedby="amount-error";
  • balance handling — over-balance detection, the exact-balance boundary, the XLM
    base-reserve subtraction (and the “never negative” case), and the unknown-balance case
    where no bogus error may be shown;
  • Max button — disabled while loading / without a known balance / without a handler,
    enabled and wired to onSetMax otherwise;
  • dependency failures — a failed balance lookup degrades to a non-blocking
    role="status" message instead of a validation error.

3. #1797 — Schedule step, timezones and DST

frontend/src/components/stream-creation/__tests__/ScheduleStep.test.tsx (new, 21 tests)

  • duration units — the three units (days, weeks, months) are rendered, changes
    are reported to the parent, and each unit converts to a fixed number of seconds
    (86 400 / 604 800 / 2 592 000). A month is explicitly 30 days, never a calendar
    month, so the on-chain duration is deterministic;
  • timezone / daylight-saving boundaries — the elapsed duration is invariant across the
    2026 US spring-forward (local day = 23 h) and fall-back (local day = 25 h) transitions,
    and identical in UTC, America/New_York, Europe/Berlin and Asia/Tokyo. The test
    derives the local-day length from Intl.DateTimeFormat (with a timezone-offset
    correction) so it stays correct on any machine, with or without full ICU;
  • duration validation — empty, zero and negative durations are rejected with the
    documented message and the inline error contract is rendered;
  • memo byte limit — the counter measures UTF-8 bytes, not characters (a single
    € is 3 bytes), the “approaching the limit” warning appears at 23 bytes, exactly 28
    bytes is accepted, 29 bytes and a 10-character/30-byte multi-byte memo are rejected.

4. #1798 — Token step and token metadata

frontend/src/components/stream-creation/__tests__/TokenStep.test.tsx (new, 19 tests)

  • selection contract — a radiogroup with one radio per supported token, correct
    aria-checked state, onChange reporting the symbol, and the error contract
    (role="alert" + aria-describedby="token-error");
  • missing token metadata — with no logo source the deterministic avatar fallback
    (initials) is rendered, and a token whose contract address is missing from the
    registry still renders its option instead of crashing;
  • failed token lookup — getTokenAddress("DOGE") throws SorobanCallError
    (Unsupported token: DOGE, code Unknown), and supported symbols resolve
    case-insensitively;
  • decimals metadata — fetchTokenDecimals returns the contract value, caches it (one
    RPC call), and falls back to Stellar’s 7 decimals when the lookup fails, when the
    contract returns no value, when the returned decimals are of an unsupported type and
    when the RPC throws. getTokenDecimalsSync returns the cached/default value
    synchronously and reports the fetched value through its callback.

5. Bug fix found by the new tests

frontend/src/hooks/useStreamForm.ts — validateAll() now returns the freshly computed
error object instead of a boolean, and frontend/src/app/streams/create/create-stream-content.tsx
reads the first error from that result:

const validationErrors = validateAll();
if (Object.keys(validationErrors).length > 0) {
  const firstError = Object.values(validationErrors)[0];
  if (firstError) toast.error(firstError);
  return;
}

Before this change the component read errors from the hook state in the same tick
as the validation, but the state update is asynchronous — so Object.values(errors)[0]
was always undefined and the “first error as a toast” UX described in the code comment
never happened. The inline error was still rendered; now the toast is shown as intended.
validateAll had exactly one caller, and no test or hook consumer relied on the boolean
return value, so the change is contained.


Files changed

 frontend/src/app/streams/create/__tests__/create-stream-content.tx-lifecycle.test.tsx | 345 +++++
 frontend/src/app/streams/create/create-stream-content.tsx                        |   9 +-
 frontend/src/components/stream-creation/__tests__/AmountStep.test.tsx            | 255 ++++++++
 frontend/src/components/stream-creation/__tests__/ScheduleStep.test.tsx          | 320 +++++++++++++
 frontend/src/components/stream-creation/__tests__/TokenStep.test.tsx            | 298 ++++++++++
 frontend/src/hooks/useStreamForm.ts                                               |  16 +-
 frontend/src/lib/soroban.polling.test.ts                                          | 238 ++++++++++

No production behaviour changes other than the toast fix described above.


Testing

All new tests live in the existing Vitest suite (frontend/vitest.config.ts collects
src/**/*.{test,spec}.{ts,tsx}), so they run automatically in the Frontend CI job
(npm run test:coverage).

frontend/src/components/stream-creation/__tests__/AmountStep.test.tsx                     22 tests
frontend/src/components/stream-creation/__tests__/ScheduleStep.test.tsx                   21 tests
frontend/src/components/stream-creation/__tests__/TokenStep.test.tsx                     19 tests
frontend/src/app/streams/create/__tests__/create-stream-content.tx-lifecycle.test.tsx    14 tests
frontend/src/lib/soroban.polling.test.ts                                                 7 tests
                                                                                    ---------
                                                                                     83 new tests

CI pre-checks run locally

Check Command Result
Frontend lint npm run lint (frontend) ✅ 0 errors (17 pre-existing warnings, none in the changed files)
Frontend type check npx tsc --noEmit ✅ no errors
Frontend tests + coverage npm run test:coverage ✅ 83 files / 790 tests passed, coverage thresholds met (all files 64.24 % stmts / 67.57 % funcs)
Frontend production build npm run build ✅ compiled successfully, 17 routes generated
Bundle budget bash ./scripts/check-bundle-size.sh ✅ 565 675 B gzipped / 614 400 B budget
Contracts formatting cargo fmt --all -- --check (contracts) ✅
Contracts lints cargo clippy --all-targets -- -D warnings (contracts) ✅
Contract tests cargo test (contracts) ✅ 275 passed, 0 failed, 1 ignored
Backend type check npx tsc --noEmit (backend) ✅ backend untouched by this PR (CI skips the backend job because backend/** does not change)

Determinism notes: no test depends on wall-clock time, network access, randomness or the
machine’s timezone. Time-related assertions use fake timers
(vi.useFakeTimers / vi.advanceTimersByTimeAsync) or explicit IANA timezones.


Reviewer notes

  • The tests are behaviour-oriented: they assert the public contract (rendered error
    messages, ARIA wiring, callbacks, toasts, redirects) rather than implementation details.
  • The DST test derives the local-day length with Intl.DateTimeFormat and an explicit
    timezone-offset correction, so it asserts the real 23 h/25 h local days instead of
    hard-coding a calendar assumption.
  • TokenStep.test.tsx overrides the token address registry through a module mock with a
    getter, which keeps the “missing metadata” path testable without touching the real
    registry.
  • The useStreamForm.validateAll signature change is the only API change; it is
    documented in the hook and covered by the new lifecycle tests.

Checklist

  • Branch created from main (test/issues-1794-1795-1797-1798)
  • Tests added for the documented success and failure paths, including boundary inputs
    and dependency failures
  • New tests run in the corresponding CI suite (npm run test:coverage)
  • Lint, type check, tests, build, bundle budget and contract checks pass locally
  • PR body references all four issues (Closes #1794, Closes #1795, Closes #1797,
    Closes #1798)

…lifecycle paths

Closes LabsCrypt#1794, Closes LabsCrypt#1795, Closes LabsCrypt#1797, Closes LabsCrypt#1798

Adds deterministic regression tests for the four modules flagged by the
testing sweep, plus a small fix to the flat create-stream form so the
first validation error is actually surfaced as a toast.

LabsCrypt#1794 — create-wizard transaction lifecycle
- frontend/src/lib/soroban.polling.test.ts: the confirmation polling loop
  (success, on-chain failure, 20-attempt timeout, wallet cancellation,
  broadcast error, simulation error, multisig routing).
- frontend/src/app/streams/create/__tests__/create-stream-content.tx-lifecycle.test.tsx:
  the page-level states (signing -> confirming -> redirect, timeout,
  failed transaction, cancellation, validation and dependency failures).

LabsCrypt#1795 — frontend/src/components/stream-creation/__tests__/AmountStep.test.tsx
  7-decimal token precision boundaries, zero/invalid amounts, over-balance
  detection, XLM base reserve, the Max button and balance lookup failures.

LabsCrypt#1797 — frontend/src/components/stream-creation/__tests__/ScheduleStep.test.tsx
  duration unit conversion to fixed seconds, timezone/DST invariant
  durations, duration validation and the 28-byte UTF-8 memo limit.

LabsCrypt#1798 — frontend/src/components/stream-creation/__tests__/TokenStep.test.tsx
  token selection and error contract, missing token metadata (avatar
  fallback), failed token lookup, and unsupported/failed decimals lookups.

Fix: useStreamForm.validateAll() now returns the freshly computed errors
instead of a boolean, so create-stream-content can report the first
validation error as a toast (the previous code read the hook state before
the state update had been applied, so the toast was never shown).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant