import { Effect } from 'effect'; import { Exit } from 'effect/Exit'; import { FieldErrors } from '@voltro/client'; import { FormBinding } from '@voltro/client'; import { FormStateSnapshot } from '@voltro/client'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RunForkOptions } from 'effect/Runtime'; import { RuntimeFiber } from 'effect/Fiber'; import { UseFormBindingOptions } from '@voltro/client'; /** The snapshot shape `useConnectionStatus` subscribes to. */ declare interface ConnectionFacts { readonly rawTransport: 'connected' | 'disconnected'; readonly status: TestConnectionStatus; readonly failureCount: number; readonly lastFailureAt: number | undefined; readonly failedSubscriptions: ReadonlyArray; readonly pendingMutationReplays: number; readonly mutationReplay: 'unknown' | 'enabled' | 'disabled'; } /** * The runtime the hooks get. EXPORTED so it can be guarded. * * `runPromiseExit` is required, not optional: every write hook goes through * `runRpcPromise`, which reads the exit and re-throws `Cause.squash(...)` so a * rejection carries its `_tag` — and it calls `runtime.runPromiseExit`, not * `runPromise`. A fake missing the method makes `mutate()` throw * `runtime.runPromiseExit is not a function` into a `void`ed promise: no handler * runs, nothing is recorded, and the component renders perfectly. This harness * kept a two-method runtime for a release after the write path moved, and the * failure read as "the click did not register". * * It is a module constant rather than a per-instance literal for one reason: * `fakeRuntimeParity.test.ts` derives the required method set from the client's * own source and checks it against THIS object, so the next method the client * starts calling goes red here instead of in whichever hook reaches it first. */ export declare const FAKE_RUNTIME: { runPromise: (effect: Effect.Effect, options?: { readonly signal?: AbortSignal | undefined; } | undefined) => Promise; runPromiseExit: (effect: Effect.Effect, options?: { readonly signal?: AbortSignal; } | undefined) => Promise< Exit>; runFork: (effect: Effect.Effect, options?: RunForkOptions) => RuntimeFiber; dispose: () => Effect.Effect; }; export declare interface FormBindingHarness, Output> { /** The live binding, as of the latest render. */ readonly binding: () => FormBinding; /** Set one field (dotted paths reach nested values), wrapped in `act`. */ readonly fill: (path: string, value: unknown) => void; /** Blur one field — what reveals its error under the default timing. */ readonly blur: (path: string) => void; /** Submit; resolves the output or `undefined` when blocked. */ readonly submit: () => Promise; /** The VISIBLE errors right now. */ readonly errors: () => FieldErrors; readonly state: () => FormStateSnapshot; /** The underlying fake api — recorded `calls`, `setSubscription`, … */ readonly client: VoltroTestClient; readonly unmount: () => void; } declare type Handler = (input: never) => unknown; export declare const makeVoltroTestClient: (config?: VoltroTestClientConfig) => VoltroTestClient; /** One recorded write, so a test can assert WHAT was called with WHICH input. */ export declare interface RecordedCall { readonly kind: 'mutation' | 'action'; readonly tag: string; readonly input: unknown; } /** * Render `useFormBinding` against a fake api and hand back form-shaped * controls: * * const form = await renderFormBinding('users.create', { * binding: { schema: UsersCreateInput }, * mutation: (input) => { * if ((input as { email: string }).email === 'taken@x.io') { * throw new ValidationError({ field: 'email', message: 'validation.emailTaken' }) * } * return { id: 'user_1' } * }, * }) * form.fill('email', 'taken@x.io') * await form.submit() * expect(form.errors()['email']).toBe('validation.emailTaken') */ export declare const renderFormBinding: = Record, Output = unknown>(tag: string, options?: RenderFormBindingOptions) => Promise>; export declare interface RenderFormBindingOptions> { /** The api name the binding asks for. Default `'app'`. */ readonly apiName?: string; /** The mutation handler for THIS tag. Return the output; throw a * `ValidationError` / `ValidationErrors` to exercise server field-error * routing; throw anything else for the loud-failure path. Default: echo * `{ ok: true }`. */ readonly mutation?: (input: unknown) => unknown | Promise; /** Extra fake-api config (other mutations, subscriptions, actions). */ readonly client?: Omit; /** Options for the binding itself — `schema` is usually the one you want * (the fake api carries no descriptors). */ readonly binding?: UseFormBindingOptions; } /** * The parts of a snapshot that are NOT the data. * * They exist because real pages render them: the notes template greys out rows * while `pendingPatches > 0` and prints the time of the last delta from * `emittedAt`. The harness hardcoded both, so a page that renders either had a * state no test could reach — the gap you only find by pointing the harness at * an app that was written before it. */ export declare interface SnapshotMeta { /** Optimistic patches awaiting confirmation. Drives "n pending" affordances. */ readonly pendingPatches?: number; /** When the server emitted this delta, in ms. */ readonly emittedAt?: number; } /** The coordinator's own three states. `'offline'` is not one of them: the * hook derives it from `navigator.onLine`, so a test that wants it stubs the * browser (`vi.spyOn(navigator, 'onLine', 'get')`), not the harness. */ export declare type TestConnectionStatus = 'connected' | 'recovering' | 'degraded'; export declare interface VoltroTestClient { /** Wrap the component under test. */ readonly Provider: (props: { readonly children: ReactNode; }) => ReactElement; /** Push new data for a tag — components re-render, like a server delta. */ readonly setSubscription: (tag: string, data: unknown, meta?: SnapshotMeta) => void; /** Put a tag into the COLD-START error state (no snapshot + an error). */ readonly failSubscription: (tag: string, error: unknown) => void; /** Return a tag to the loading state (no snapshot yet). */ readonly resetSubscription: (tag: string) => void; /** Every mutation/action invoked so far, in order. */ readonly calls: ReadonlyArray; /** * Drive what `useConnectionStatus` reports — the coordinator's presentation * state plus, optionally, the facts beside it (`failureCount`, * `failedSubscriptions`, `pendingMutationReplays`, `rawTransport`). * Components re-render, like a real transport gap. Without this, a page's * disconnect banner had a state no test could reach except by mocking the * whole client module. */ readonly setConnectionStatus: (status: TestConnectionStatus, facts?: Partial>) => void; /** How many times a component asked the cache to refresh (`useRefreshSubscriptions`). */ readonly refreshes: number; /** * Emit an rpc error onto the api's error bus — what `useOnRpcError` / * `useRpcErrors` and the presentation layer subscribe to. `kind: 'transport'` * with `outcome: 'unknown'` is the raw channel a socket cut produces; * `kind: 'handler'` is a typed failure the presentation layer shows. A * banner that reacts to either had no way to be handed one. */ readonly emitRpcError: (event: { readonly source: 'mutation' | 'action' | 'subscription'; readonly tag: string; readonly error: unknown; readonly kind: 'transport' | 'handler' | 'client' | 'unknown'; readonly outcome?: 'unknown' | 'known' | 'not-started'; readonly traceId?: string; }) => void; } export declare interface VoltroTestClientConfig { /** The api name your components pass to the hooks. Default `'app'`. */ readonly apiName?: string; /** Initial subscription data per rpc tag. A tag that is ABSENT stays in the * loading state — that distinction is exactly what you want to test. */ readonly subscriptions?: Readonly>; /** Mutation handlers per tag. Throw (or reject) to exercise the failure path. */ readonly mutations?: Readonly>; /** Action handlers per tag. */ readonly actions?: Readonly>; } export { }