import { BlockDefinition, ComponentRegistryEntry } from "@ministryofjustice/hmpps-forge/core/components"; import { CompilationTrace, CompilationTraceError, CompilationTraceEvent, CompilationTracePhase, EffectFunctionContext, ForgeExecutionRequest, ForgeInstrumentationOptions, ForgePackageFunctions, ForgePackageRegistration, HookType, RequestTrace, RequestTraceEvent, RequestTracePhase, RequestTraceUnit, RuntimeContext, SerializedTraceSpan, ValidationResult } from "@ministryofjustice/hmpps-forge/core"; import { BaseFunctionRegistry, ChainableGenerator, ConditionFunctionExpr, EffectFunctionExpr, FunctionEvaluator, GeneratorFunctionExpr, TransformerFunctionExpr } from "@ministryofjustice/hmpps-forge/core/authoring"; import { CookieMutation, CookieOptions, ForgeError, ForgeOutcome, ForgeRenderer, ForgeTopology, Logger, RenderBlock, RenderContext, ResponseBindings } from "@ministryofjustice/hmpps-forge/core/framework"; //#region forge-core/src/testing/test-client/testResult.type.d.ts /** Options for configuring a test request sent via {@link ForgeTestClient}. */ interface TestRequestOptions { headers?: Record; cookies?: Record; params?: Record; query?: Record; body?: Record; session?: unknown; state?: Record; } /** Result returned when the engine renders a step. */ interface TestRenderResult { type: 'render'; context: RenderContext; /** Assembled renderer output. Present only when the client was created with a renderer. */ output?: unknown; headers: Map; cookies: Map; getBlocksByVariant(variant: string): RenderBlock[]; getValidationErrorsByFieldCode(fieldCode: string): ValidationResult[]; } /** Result returned when the engine redirects (navigation, access denial, etc.). */ type TestRedirectResult = { type: 'redirect'; url: string; headers: Map; cookies: Map; }; /** Result returned when the engine yields an error outcome. */ type TestErrorResult = { type: 'error'; error: ForgeError; headers: Map; cookies: Map; }; /** Discriminated union returned by {@link ForgeTestClient.get} and {@link ForgeTestClient.post}. */ type TestResult = TestRenderResult | TestRedirectResult | TestErrorResult; //#endregion //#region forge-core/src/testing/test-client/ForgeTestClient.d.ts interface TestClientForge { getTopology(): ForgeTopology; execute(request: ForgeExecutionRequest): Promise>; } declare class ForgeTestClient { private readonly forge; private readonly renderer?; private capturedHeaders; private capturedCookies; constructor(forge: TestClientForge, renderer?: ForgeRenderer | undefined); get(path: string, options?: TestRequestOptions): Promise; post(path: string, options?: TestRequestOptions): Promise; private dispatch; private createResponseBindings; private buildResult; } //#endregion //#region forge-core/src/testing/test-client/ForgeTestHarness.d.ts interface ForgeTestHarnessOptions { readonly instrumentation?: ForgeInstrumentationOptions; readonly maxIteratorIterations?: number; readonly strictRegistration?: boolean; readonly logger?: Logger | Console; readonly disableBuiltInFunctions?: boolean; readonly disableBuiltInComponents?: boolean; readonly basePath?: string; } /** * Convenience wrapper for testing Forge journeys without boilerplate. * * Wires up the test adapter and a silent logger internally so tests * only need to register packages and call `createClient()`. * * @example * ```typescript * const client = new ForgeTestHarness() * .registerGlobalComponents(govukComponents) * .registerPackage(createForgePackage({ journey: myJourney, functions: myEffects }), deps) * .createClient() * * const result = await client.get('/my-journey/step-one', { session: {} }) * expect(result.type).toBe('render') * ``` */ declare class ForgeTestHarness { private readonly forge; constructor(options?: ForgeTestHarnessOptions); registerGlobalComponents(components: ComponentRegistryEntry[]): this; registerGlobalFunctions(functions: ForgePackageFunctions, deps?: TDeps): this; registerPackage(pkg: ForgePackageRegistration, deps?: TDeps): this; createClient(renderer?: ForgeRenderer): ForgeTestClient; } //#endregion //#region forge-core/src/testing/functions/FunctionRegistryTestHarness.d.ts /** * Unit-tests functions registered in a `ConditionRegistry`, `TransformerRegistry`, * `EffectRegistry`, or `GeneratorRegistry` through the engine's real evaluation * pipeline — schema prechecks, short-circuits, and output validation — rather than * calling the raw evaluator and bypassing all of it. * * Pass the value returned by the author-facing handle that `register(...)` gives * back. `evaluate` then supplies the argument the engine injects at runtime: * `withInput` for conditions and transformers, `withContext` for effects. * Generators take no injected argument, so `evaluate` runs them immediately. * * @example * ```typescript * const conditions = new ConditionRegistry() * const isRequired = conditions.register('isRequired', { factory: () => (value) => value != null }) * * const harness = new FunctionRegistryTestHarness(conditions) * expect(harness.evaluate(isRequired()).withInput('hello')).toBe(true) * expect(harness.evaluate(isRequired()).withInput(undefined)).toBe(false) * ``` * * @example * ```typescript * const effects = new EffectRegistry() * const stamp = effects.register('stamp', { factory: () => (context) => context.setAnswer('stamped', true) }) * * const context = createTestEffectContext() * new FunctionRegistryTestHarness(effects).evaluate(stamp()).withContext(context) * expect(context.getAnswer('stamped')).toBe(true) * ``` */ declare class FunctionRegistryTestHarness> { private readonly entries; constructor(functions: BaseFunctionRegistry | BaseFunctionRegistry[], deps?: TDeps); evaluate(expr: GeneratorFunctionExpr | ChainableGenerator): unknown; evaluate(expr: ConditionFunctionExpr): { withInput(value: unknown): unknown; }; evaluate(expr: TransformerFunctionExpr): { withInput(value: unknown): unknown; }; evaluate(expr: EffectFunctionExpr): { withContext(context: EffectFunctionContext): unknown; }; private lookup; private execute; } //#endregion //#region forge-core/src/testing/functions/createTestPackage.d.ts interface TestPackageOptions { /** Function evaluators to replace in the package, keyed by function name. */ overrides?: Record; } /** * Create a copy of a Forge package with specific function implementations replaced. * * Overrides replace specific functions by name. Functions not listed * in overrides keep their original implementation. * * @example * ```typescript * const mockSendEmail = vi.fn() * const pkg = createTestPackage(myPackage, { * overrides: { SendEmail: mockSendEmail }, * }) * * forge.registerPackage(pkg, { api: mockApi }) * * // After a request: * expect(mockSendEmail).not.toHaveBeenCalled() * ``` */ declare function createTestPackage(pkg: ForgePackageRegistration, options?: TestPackageOptions): ForgePackageRegistration; //#endregion //#region forge-core/src/testing/assertions/outcomeAssertions.d.ts declare function expectRenderOutcome(result: TestResult): asserts result is TestRenderResult; declare function expectRedirectOutcome(result: TestResult): asserts result is TestRedirectResult; declare function expectErrorOutcome(result: TestResult): asserts result is TestErrorResult; //#endregion //#region forge-core/src/testing/assertions/ForgeTestOutcomeAssertionError.d.ts declare class ForgeTestOutcomeAssertionError extends Error { constructor(message: string); } //#endregion //#region forge-core/src/testing/functions/createTestEffectContext.d.ts /** * In-memory seed for {@link createTestEffectContext}. Every field is optional and * defaults to empty; `answers` takes plain current values, not answer histories. */ interface EffectContextSeed { answers?: Record; data?: Record; session?: Record; params?: Record; query?: Record; post?: Record; state?: Record; headers?: Record; cookies?: Record; url?: string; hookType?: HookType; } /** * Captures headers and cookies written through {@link ResponseBindings} so tests can * read back what an effect set — the base context can write these but not read them. */ declare class RecordingResponseBindings implements ResponseBindings { private readonly headers; private readonly cookies; setHeader(name: string, value: string): void; setCookie(name: string, value: string, options?: CookieOptions): void; getHeaders(): Record; getCookies(): Record; } /** * A real {@link EffectFunctionContext} with two extra getters that expose the response * headers and cookies an effect wrote via `setResponseHeader`/`setResponseCookie`. */ declare class TestEffectContext extends EffectFunctionContext { private readonly recordingResponse; constructor(context: RuntimeContext, recordingResponse: RecordingResponseBindings, hookType: HookType); getResponseHeaders(): Record; getResponseCookies(): Record; } /** * Build a real {@link EffectFunctionContext} over minimal in-memory state, so * effect-function tests can exercise the genuine context instead of hand-rolling a fake. * * @example * ```typescript * const context = createTestEffectContext({ * answers: { goalDescription: 'Learn TypeScript' }, * hookType: 'submit', * }) * * myEffect(context) * * expect(context.getResponseHeaders()['x-audited']).toBe('true') * ``` */ declare function createTestEffectContext(seed?: EffectContextSeed): TestEffectContext; //#endregion export { type CompilationTrace, type CompilationTraceError, type CompilationTraceEvent, type CompilationTracePhase, type EffectContextSeed, ForgeTestClient, ForgeTestHarness, type ForgeTestHarnessOptions, ForgeTestOutcomeAssertionError, FunctionRegistryTestHarness, type RequestTrace, type RequestTraceEvent, type RequestTracePhase, type RequestTraceUnit, type SerializedTraceSpan, TestEffectContext, type TestErrorResult, type TestPackageOptions, type TestRedirectResult, type TestRenderResult, type TestRequestOptions, type TestResult, createTestEffectContext, createTestPackage, expectErrorOutcome, expectRedirectOutcome, expectRenderOutcome };