ngx-json-render 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-json-render",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Angular renderer for json-render — stream AI-generated JSON specs into real Angular components with signals.",
5
5
  "keywords": [
6
6
  "angular",
@@ -40,6 +40,10 @@
40
40
  ".": {
41
41
  "types": "./types/ngx-json-render.d.ts",
42
42
  "default": "./fesm2022/ngx-json-render.mjs"
43
+ },
44
+ "./testing": {
45
+ "types": "./types/ngx-json-render-testing.d.ts",
46
+ "default": "./fesm2022/ngx-json-render-testing.mjs"
43
47
  }
44
48
  },
45
49
  "type": "module"
@@ -0,0 +1,197 @@
1
+ import { Spec, StateStore, ActionHandler, ComputedFunction, ValidationFunction, DirectiveDefinition } from '@json-render/core';
2
+ import { Type, Provider } from '@angular/core';
3
+ import { ComponentFixture } from '@angular/core/testing';
4
+ import { ComponentRegistry, StateModel, RegistryEntry, JsonRenderer, JsonRenderStateService, JsonRenderValidationService, StateChange } from 'ngx-json-render';
5
+
6
+ /**
7
+ * The lines of one recorded response body, in the order the server sent them,
8
+ * without their trailing newlines.
9
+ */
10
+ type Recording = readonly string[];
11
+ /**
12
+ * What to replay for a request. Either one recording for every request, a
13
+ * lookup by prompt, or a function for anything more involved.
14
+ */
15
+ type Recordings = Recording | Readonly<Record<string, Recording>> | ((prompt: string, body: Readonly<Record<string, unknown>>) => Recording | undefined);
16
+ /** An error the recorded server answers with instead of a stream. */
17
+ interface RecordedFailure {
18
+ /** HTTP status. Defaults to 500. */
19
+ status?: number;
20
+ /** The `message` field of the JSON error body. */
21
+ message?: string;
22
+ }
23
+ /** Options for {@link recordedTransport}. */
24
+ interface RecordedTransportOptions {
25
+ /**
26
+ * Milliseconds between lines — the pace a model would emit them at.
27
+ * Defaults to 0, which still yields to the event loop between lines, so a
28
+ * test can observe the stream in flight.
29
+ */
30
+ delayMs?: number;
31
+ /**
32
+ * Answer with an error instead of a stream. A function is called per
33
+ * request, so a test can turn the failure on and off between sends.
34
+ */
35
+ fail?: RecordedFailure | (() => RecordedFailure | null | undefined);
36
+ /**
37
+ * Pull the prompt out of the request body. The default reads `prompt`
38
+ * (what `injectUIStream` sends) and falls back to the last message's
39
+ * content (what `injectChatUI` sends).
40
+ */
41
+ promptOf?: (body: Readonly<Record<string, unknown>>) => string;
42
+ }
43
+ /**
44
+ * A `fetch` that answers the streaming hooks from a recording instead of a
45
+ * server.
46
+ *
47
+ * Both {@link injectUIStream} and {@link injectChatUI} take any function with
48
+ * fetch's shape, so a test runs the real client — request body, streamed
49
+ * lines, usage metadata, abort on supersede — with no server, no API key and
50
+ * no global patched behind the test's back. Only the transport is recorded.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const ui = injectUIStream({
55
+ * api: '/api/generate',
56
+ * fetch: recordedTransport(specStream(spec)),
57
+ * });
58
+ * ```
59
+ *
60
+ * @example Different answers per prompt, at a visible pace:
61
+ * ```ts
62
+ * recordedTransport(
63
+ * { 'a dashboard': specStream(dashboard), 'a form': specStream(form) },
64
+ * { delayMs: 20 },
65
+ * );
66
+ * ```
67
+ */
68
+ declare function recordedTransport(recordings: Recordings, options?: RecordedTransportOptions): typeof globalThis.fetch;
69
+ /**
70
+ * The JSONL patch lines a model would emit to build `spec`: the root, then
71
+ * the state, then one line per element in declaration order.
72
+ *
73
+ * Recording a stream by hand means writing patches by hand; this writes them
74
+ * from the spec a test already has, so the test says what it renders rather
75
+ * than how the wire spells it.
76
+ */
77
+ declare function specStream(spec: Spec): string[];
78
+ /**
79
+ * The usage metadata line `injectUIStream` reads into its `usage` signal.
80
+ * Append it to a recording to exercise that path.
81
+ */
82
+ declare function usageLine(usage: {
83
+ promptTokens?: number;
84
+ completionTokens?: number;
85
+ totalTokens?: number;
86
+ }): string;
87
+
88
+ /** One action the spec asked for, in the order it was dispatched. */
89
+ interface DispatchedAction {
90
+ name: string;
91
+ params?: Record<string, unknown>;
92
+ }
93
+ /** Options for {@link renderSpec}. Everything but `registry` is optional. */
94
+ interface RenderSpecOptions {
95
+ /** Catalog type name → Angular component. The one required option. */
96
+ registry: ComponentRegistry;
97
+ /** Initial state model. Defaults to the spec's own `state`. */
98
+ state?: StateModel;
99
+ /** External store, for controlled mode. */
100
+ store?: StateStore | null;
101
+ /** Action handlers by name. Dispatches are recorded either way. */
102
+ handlers?: Record<string, ActionHandler>;
103
+ /** Catch-all handler for actions with no dedicated handler. */
104
+ onAction?: (name: string, params?: Record<string, unknown>) => unknown;
105
+ /** Navigation function for `onSuccess: { navigate }`. */
106
+ navigate?: (path: string) => void;
107
+ /** Fallback component for unknown types. */
108
+ fallback?: Type<unknown> | RegistryEntry | null;
109
+ /** Named functions for `$computed` props. */
110
+ functions?: Record<string, ComputedFunction>;
111
+ /** Custom validation functions. */
112
+ validationFunctions?: Record<string, ValidationFunction>;
113
+ /** Custom directives for `$`-prefixed values. */
114
+ directives?: DirectiveDefinition[];
115
+ /** Render as if the spec were still streaming. */
116
+ loading?: boolean;
117
+ /** Extra TestBed providers, e.g. a `JR_CONFIRM_DIALOG` replacement. */
118
+ providers?: Provider[];
119
+ }
120
+ /**
121
+ * A rendered spec, and the handful of things a test does to one.
122
+ *
123
+ * `fixture` and `element` are there for everything this does not cover — the
124
+ * harness is a shortcut, not a wall.
125
+ */
126
+ interface SpecHarness {
127
+ /** The fixture of the host that owns the `<json-render>`. */
128
+ readonly fixture: ComponentFixture<unknown>;
129
+ /** The host element, the root of the rendered DOM. */
130
+ readonly element: HTMLElement;
131
+ /** The renderer instance. */
132
+ readonly renderer: JsonRenderer;
133
+ /** The renderer's state store. */
134
+ readonly store: JsonRenderStateService;
135
+ /** The renderer's validation service. */
136
+ readonly validation: JsonRenderValidationService;
137
+ /** Every action the spec dispatched, handled or not, in order. */
138
+ readonly dispatched: readonly DispatchedAction[];
139
+ /** Every state change the renderer emitted, flattened, in order. */
140
+ readonly changes: readonly StateChange[];
141
+ /** Render a different spec (a later streaming frame, say). */
142
+ setSpec(spec: Spec | null): Promise<void>;
143
+ /** Flip the loading flag catalog components see. */
144
+ setLoading(loading: boolean): Promise<void>;
145
+ /** Let effects, promises and change detection finish. */
146
+ settle(): Promise<void>;
147
+ /** Read a state path, e.g. `/form/email`. */
148
+ read(path: string): unknown;
149
+ /** Write a state path and settle. */
150
+ write(path: string, value: unknown): Promise<void>;
151
+ /** The whole state model. */
152
+ state(): StateModel;
153
+ /** Trimmed text of the first match, or of everything when given nothing. */
154
+ text(selector?: string): string;
155
+ /** Trimmed text of every match. */
156
+ texts(selector: string): string[];
157
+ /** The first match. Throws when there is none. */
158
+ find<T extends HTMLElement>(selector: string): T;
159
+ /** Every match, possibly none. */
160
+ findAll<T extends HTMLElement>(selector: string): T[];
161
+ /** Click an element (or the first match of a selector) and settle. */
162
+ click(target: string | Element): Promise<void>;
163
+ /** Set an input's value, fire `input`, and settle. */
164
+ fill(target: string | Element, value: string): Promise<void>;
165
+ /** Destroy the fixture. Rarely needed — TestBed resets between tests. */
166
+ destroy(): void;
167
+ }
168
+ /**
169
+ * Render a spec against a registry and hand back the few things a test does
170
+ * to it.
171
+ *
172
+ * Testing a catalog component otherwise means a host component, a TestBed
173
+ * module, a zoneless provider, two settle passes and a `querySelector` — the
174
+ * same twenty lines in every project that adopts this package, none of them
175
+ * about the component under test.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const ui = await renderSpec(
180
+ * {
181
+ * root: 'save',
182
+ * elements: {
183
+ * save: { type: 'Button', props: { label: 'Save' }, on: { press: { action: 'save' } } },
184
+ * },
185
+ * },
186
+ * { registry: { Button: MyButton } },
187
+ * );
188
+ *
189
+ * expect(ui.text('button')).toBe('Save');
190
+ * await ui.click('button');
191
+ * expect(ui.dispatched).toEqual([{ name: 'save', params: {} }]);
192
+ * ```
193
+ */
194
+ declare function renderSpec(spec: Spec | null, options: RenderSpecOptions): Promise<SpecHarness>;
195
+
196
+ export { recordedTransport, renderSpec, specStream, usageLine };
197
+ export type { DispatchedAction, RecordedFailure, RecordedTransportOptions, Recording, Recordings, RenderSpecOptions, SpecHarness };